MCP:从创建 Server 到 Client 调用
本文使用官方 MCP Python SDK v2,对应 MCP 2026-07-28 规范,核对日期为 2026-09-18。旧教程中的
FastMCP、initialize、Mcp-Session-Id或独立 HTTP+SSE 通道可能属于旧版 SDK 或 legacy 协议,照着运行前要先确认版本。
1. MCP 为什么会出现
先设想一个文件搜索工具。它能在项目中查找代码,最初只接入一个 IDE 时,开发者自己约定一套函数参数就够了。后来桌面助手、命令行 Agent 和另一款 IDE 也想使用它,每个应用都要重新解决一遍这些问题:
- 怎样启动本地搜索进程,或者连接远程服务;
- 怎样获取工具名称、说明和参数 Schema;
- 怎样发送调用并接收结果;
- 怎样读取文件资源、提供提示词模板;
- 怎样处理版本、认证、超时和错误。
如果十个应用分别接十种外部能力,大家会写出许多相似但互不兼容的适配代码。工具作者也得为不同应用维护不同插件。
MCP 在应用与能力提供方之间放了一套共同约定:能力提供方实现 MCP Server,应用通过 MCP Client 连接它,Host 再决定哪些能力可以交给模型或用户。
没有 MCP
IDE A ──自己的适配代码── 文件搜索
IDE B ──另一套适配代码── 文件搜索
桌面助手 ──第三套插件─── 文件搜索
使用 MCP
IDE A ─┐
IDE B ─┼─ MCP Client ── MCP Server:文件搜索
桌面助手 ─┘MCP 主要解决“怎样连接、发现和调用外部能力”。它不会替 Server 判断用户是否有权读取某个文件,也不会替模型决定下一步该调用什么工具;权限、业务规则和模型决策仍由 Host、Server 和应用代码负责。
2. MCP 与 Function Calling 有什么区别
Function Calling 关注模型与应用之间的一次结构化对话。应用把工具定义交给模型,模型返回“调用 search_project,参数是 query=PAYMENT_TIMEOUT”。模型只是提出调用,真正执行函数的仍是应用。
MCP 关注的是另一段路:应用去哪里取得 search_project 的定义,怎样连接提供它的 Server,又怎样把调用发过去并拿回结果。
| 对比项 | Function Calling | MCP |
|---|---|---|
| 主要解决的问题 | 模型怎样表达“我要调用哪个工具、参数是什么” | Host 怎样发现、连接并调用外部 Server 的能力 |
| 交互双方 | 模型 API 与应用 | MCP Client 与 MCP Server |
| 工具定义从哪里来 | 应用随模型请求传入 | Client 可通过 tools/list 从 Server 获取 |
| 是否规定连接方式 | 不规定,应用自己执行函数或请求服务 | 规定 stdio、Streamable HTTP 和协议消息 |
| 能力范围 | 主要是供模型调用的函数工具 | Tools、Resources 和 Prompts |
| 谁执行真实动作 | 应用或它调用的后端 | MCP Server 中注册的函数或后端系统 |
两者经常一起工作,并不是二选一。文件搜索工具的一种常见接法是:
MCP Client 通过 tools/list 取得 search_project 的定义
↓
Host 把它转换成模型能读懂的 Function Calling 工具
↓
模型提出:调用 search_project,并给出 query 参数
↓
Host 检查参数、权限和用户确认
↓
MCP Client 通过 tools/call 请求 MCP Server 执行
↓
结果回到 Host,再交给模型组织答案可以记住一句话:Function Calling 让模型提出工具调用,MCP 让应用用统一方式找到并执行外部工具。 只有一个应用内部的小函数时,直接使用 Function Calling 往往更简单;同一能力需要被多个 Host、IDE 或 Agent 复用时,MCP 才更有价值。
3. 这次交互里有谁
接下来做一个很小的 MCP 服务:在指定项目中搜索文本。用户问 IDE:“支付接口的超时配置写在哪里?”模型决定调用 search_project,MCP Client 把调用交给文件搜索 Server,Server 找到文件后返回路径和行号。
先用官方 SDK 把它跑起来,再看 Client 和 Server 之间到底交换了什么。
用户
↓
Host:IDE,管理模型、界面、权限和会话
├── 模型:决定是否调用 search_project
└── MCP Client:连接 Server,并执行 list_tools、call_tool
↓
MCP Server:真正搜索文件模型不会自己运行 Python 函数。它只会生成一份结构化调用请求。Host 检查权限和参数后,才让 MCP Client 调用 Server。
Server 也不必包含模型。文件搜索 Server 就是一个普通 Python 程序,只是用 MCP 规定的方式公布和执行能力。
4. 安装官方 Python SDK
官方 Python SDK v2 要求 Python 3.10 或更高版本。需要命令行工具时安装 cli extra:
uv add "mcp[cli]"也可以使用 pip:
python -m pip install "mcp[cli]"安装后主要会用到这些对象:
from mcp import Client, StdioServerParameters
from mcp.server import MCPServerMCPServer用来创建 Server,并注册 Tool、Resource 和 Prompt;Client用来连接 Server;StdioServerParameters描述本地子进程怎样启动。
5. 用 MCPServer 创建文件搜索服务
先创建 server.py:
import os
from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from pydantic import BaseModel
PROJECT_ROOT = Path(os.environ.get("PROJECT_ROOT", ".")).resolve()
mcp = MCPServer(
"Project Search",
instructions="只允许搜索 PROJECT_ROOT 目录中的文件。",
)MCPServer("Project Search") 创建一个 Server 实例。instructions 会告诉 Host 这个 Server 应怎样使用,但它只是说明,不能代替后面的路径检查。
用 @mcp.tool() 注册工具
定义一下搜索结果的结构:
class Match(BaseModel):
path: str
line: int
preview: str
class SearchResult(BaseModel):
matches: list[Match]再把普通 Python 函数注册成 Tool:
@mcp.tool(title="Search project files")
def search_project(query: str, limit: int = 20) -> SearchResult:
"""在项目目录中搜索文本,返回匹配文件、行号和当前行内容。"""
if not query.strip():
raise ToolError("query 不能为空")
matches: list[Match] = []
for path in PROJECT_ROOT.rglob("*.py"):
text = path.read_text(encoding="utf-8", errors="ignore")
for line_number, line in enumerate(text.splitlines(), start=1):
if query.lower() not in line.lower():
continue
matches.append(
Match(
path=str(path.relative_to(PROJECT_ROOT)),
line=line_number,
preview=line.strip(),
)
)
if len(matches) >= limit:
return SearchResult(matches=matches)
return SearchResult(matches=matches)这里没有手写 JSON Schema。SDK 会从 query: str、limit: int 和返回的 Pydantic 模型生成工具定义、输入 Schema 与结构化输出。
ToolError 表示工具按预期拒绝了这次调用。例如搜索词为空时,Client 会收到 is_error=True 的工具结果,而不是让整个 MCP 连接崩掉。
用 @mcp.resource() 暴露文件
搜索完成后,模型可能还要读取命中的文件。可以把项目文件注册成带 URI Template 的 Resource:
@mcp.resource("project://{path}")
def read_project_file(path: str) -> str:
"""读取项目目录中的 UTF-8 文本文件。"""
target = (PROJECT_ROOT / path).resolve()
if PROJECT_ROOT not in target.parents:
raise ValueError("path 超出项目目录")
if not target.is_file():
raise ValueError("文件不存在")
return target.read_text(encoding="utf-8")路径检查不能省。project://../../secret.txt 不能穿过 PROJECT_ROOT 去读取别的目录。
用 @mcp.prompt() 注册提示词模板
如果 IDE 需要一个用户主动选择的“审查文件”模板,可以继续添加:
@mcp.prompt(title="Review a project file")
def review_file(path: str) -> str:
"""生成代码审查任务。"""
return f"请审查 project://{path},指出明确的缺陷、风险和缺失测试。"到这里,Server 已经有一个 Tool、一个 Resource Template 和一个 Prompt。代码里没有手写 tools/list 或 tools/call 分发器,这些协议细节由官方 SDK 处理。
让文件可以直接运行
在 server.py 末尾加入:
if __name__ == "__main__":
mcp.run()直接运行时,mcp.run() 使用 stdio。调试阶段也可以交给官方 Inspector:
uv run mcp dev server.py6. 用官方 Client 通过 stdio 调用
stdio 适合本地文件、Git 和开发工具。Client 启动 Server 子进程,通过 stdin/stdout 交换协议消息,退出上下文时再关闭子进程。
创建 client_stdio.py:
import anyio
from mcp import Client, StdioServerParameters
server = StdioServerParameters(
command="uv",
args=["run", "server.py"],
env={"PROJECT_ROOT": "/absolute/path/to/your/project"},
)
async def main() -> None:
async with Client(server) as client:
listed = await client.list_tools()
print([tool.name for tool in listed.tools])
result = await client.call_tool(
"search_project",
{"query": "PAYMENT_TIMEOUT", "limit": 5},
)
print(result.structured_content)
print(result.is_error)
if __name__ == "__main__":
anyio.run(main)运行 Client:
uv run client_stdio.pyasync with Client(server) 开始时,SDK 启动 uv run server.py;退出时,SDK 关闭 stdin,等待子进程结束,必要时再终止它。业务代码不用自己管理 subprocess.Popen。
正常情况下会看到类似结果:
['search_project']
{'matches': [{'path': 'src/payment.py', 'line': 18, 'preview': 'PAYMENT_TIMEOUT = 15'}]}
Falsecontent 主要给模型读取,structured_content 主要给应用代码读取,is_error 表示这次工具执行是否失败。Client 应先检查 is_error,再信任结构化结果。
stdio 模式还有一条硬规则:Server 的 stdout 只能输出协议消息。调试信息使用标准 logging 模块或 stderr,不要在 Tool 中随手 print()。
7. Host 中的 mcpServers 配置怎样写
上面的 client_stdio.py 是自己写 Client。如果使用 Claude Desktop、IDE 或其他 MCP Host,通常不需要再写 Client 程序,而是在 Host 配置中说明“怎样启动这个 Server”。
以 Claude Desktop 的本地配置为例:
{
"mcpServers": {
"project-search": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/project-search-server",
"run",
"server.py"
],
"env": {
"PROJECT_ROOT": "/absolute/path/to/your/project"
}
}
}
}这几个字段分别表示:
| 字段 | 作用 |
|---|---|
mcpServers | Host 管理的 MCP Server 集合 |
project-search | 自己起的 Server 名称,会显示在 Host 中 |
command | Host 要启动的可执行程序 |
args | 传给命令的参数 |
env | 只传给该 Server 的环境变量 |
mcpServers 不是 MCP 协议消息,也不是所有 Host 必须使用的标准配置文件。它是许多 Host 采用的配置约定;不同 IDE 可能增加 cwd、disabled 或权限字段,实际字段要以对应 Host 文档为准。
Claude Desktop 的配置文件通常位于:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json保存后需要重启 Host。若 Server 没有出现,先在终端中单独运行 uv run server.py,再检查 command、绝对路径和 Host 日志。
配置中不要随意放长期密钥。即使某个 Host 支持 env,也要考虑配置文件的读取权限,优先使用系统密钥管理、短期凭据或 OAuth。
8. 换成 Streamable HTTP
stdio Server 只能由本机 Host 启动。如果多个 IDE、网页应用或团队需要共用同一个 Server,可以改用 Streamable HTTP。
启动 HTTP Server
官方 CLI 可以直接运行刚才的 mcp 实例:
uv run mcp run server.py --transport streamable-http默认示例地址是:
http://localhost:8000/mcp先回答路径问题:最新版仍然常用 /mcp,但规范没有强制路径必须叫 /mcp。 Streamable HTTP 规范要求 Server 提供一个支持 POST 的单一 MCP endpoint,https://example.com/mcp 只是规范给出的例子。
截至 2026-09-18,官方 Python SDK 最新稳定版为 v2.2.0,MCPServer 的 streamable_http_path 默认值仍是 /mcp。这就是大多数示例使用 http://localhost:8000/mcp 的原因。它是 SDK 默认值,不是协议保留字;需要挂在统一网关下面时,可以改路径:
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
streamable_http_path="/internal/project-search",
)此时 Client 应连接 http://localhost:8000/internal/project-search,不能继续使用 /mcp。SDK 中的默认参数可在官方 Python SDK v2.2.0 源码中核对。
写完 Server 后,到底开放了哪些 API
这里容易混淆,是因为“API”同时指了三个层次:
| 层次 | 文件搜索 Server 中的例子 | 负责什么 |
|---|---|---|
| HTTP 入口 | POST /mcp | 把 MCP 消息送到 Server,路径可以配置 |
| MCP 协议方法 | tools/list、tools/call | 说明本次请求要查询还是调用工具 |
| 业务能力 | search_project | 你用 @mcp.tool() 注册的具体工具 |
可以把 /mcp 想成办事大厅唯一的窗口。Client 每次都到这个窗口,但递交的表单不同:tools/list 是“给我看看有哪些工具”,tools/call 是“请执行某个工具”,search_project 才是表单中填写的具体工具名。
这只适用于 Streamable HTTP。使用 stdio 时,Server 不监听端口,也没有 /mcp 或其他 HTTP API;同样的 tools/list、tools/call 消息通过进程的 stdin/stdout 传输。
因此,下面这段 Server 代码不会创建 POST /search_project:
@mcp.tool()
def search_project(query: str) -> SearchResult:
return search_files(query)它做的是把 search_project 注册进 MCP 工具列表。一次完整调用是这样发生的:
- Client 向 MCP endpoint 发送
tools/list。 - Server 返回
search_project的名称、说明和参数 Schema。 - Host 或模型决定调用它。
- Client 再发送
tools/call,并在params.name中填写search_project。 - SDK 找到被
@mcp.tool()装饰的 Python 函数,执行后把结果放回 MCP 响应。
前后两次请求都进入同一个 HTTP endpoint:
POST /mcp
body.method = "tools/list"
POST /mcp
body.method = "tools/call"
body.params.name = "search_project"如果 Server 还注册了 Resource 和 Prompt,同一个 endpoint 还可以接收 resources/list、resources/read、prompts/list 和 prompts/get。这些同样不是独立 URL:project://src/payment.py 是 Resource URI,不是浏览器要访问的 HTTP 路径。没有注册某类能力时,Client 应根据 server_capabilities 和 list 结果判断是否可用。
部署时 endpoint 可以改成 /ai/mcp 等其他路径,但 Client 和反向代理必须使用 Server 实际配置的完整 URL。不要根据 Tool 名称猜成 /search_project、/tools/list 或 /api/mcp/tools/call。
HTTP Client 只需传入 URL
创建 client_http.py:
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
listed = await client.list_tools()
print([tool.name for tool in listed.tools])
result = await client.call_tool(
"search_project",
{"query": "PAYMENT_TIMEOUT", "limit": 5},
)
print(result.structured_content)
if __name__ == "__main__":
anyio.run(main)传给 Client 的是 URL 字符串时,SDK 自动选择 Streamable HTTP;传入 StdioServerParameters 时,SDK 自动选择 stdio。后面的 list_tools() 和 call_tool() 写法完全相同。
需要认证时怎样传 Header
官方 SDK v2 把 Header、代理、mTLS 和超时交给 httpx2.AsyncClient 管理:
import anyio
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
async def main() -> None:
async with httpx2.AsyncClient(
headers={"Authorization": "Bearer <access-token>"},
timeout=httpx2.Timeout(30.0, read=300.0),
) as http_client:
transport = streamable_http_client(
"https://mcp.example.com/mcp",
http_client=http_client,
)
async with Client(transport) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
if __name__ == "__main__":
anyio.run(main)旧版示例可能把 headers= 直接传给 streamable_http_client()。SDK v2 已不这样使用,HTTP 相关配置放在传入的 httpx2.AsyncClient 上。
9. 远程 Server 的 Host 配置
远程 Server 通常已经运行,Host 不需要 command 和 args,只需要它的完整 URL。Claude 当前通过设置中的 Custom Connector 添加远程地址,例如:
https://mcp.example.com/mcp部分 IDE 和开发工具也允许把远程 Server 写进项目配置,常见形状如下:
{
"mcpServers": {
"project-search-remote": {
"type": "http",
"url": "https://mcp.example.com/mcp"
}
}
}type、url、headers 和环境变量引用方式并不是 MCP 规范统一规定的。Claude Desktop、Claude Code、Cursor、VS Code 与其他 Host 的远程配置格式可能不同,不能把一家的配置原样复制到所有客户端。
涉及 OAuth 时,优先让 Host 按远程 Server 公布的授权信息完成登录,不要把长期 Bearer Token 写死在项目仓库中。
10. 常用 SDK API 与协议方法怎样对应
使用 SDK 时调用 Python 方法;查看日志或排查兼容问题时,才需要关心底层协议方法。
| Server 端 | Client 端 | 底层协议方法 | 用途 |
|---|---|---|---|
@mcp.tool() | client.list_tools() | tools/list | 查询工具定义 |
@mcp.tool() | client.call_tool() | tools/call | 调用工具 |
@mcp.resource() | client.list_resources() | resources/list | 查询固定 URI 资源 |
@mcp.resource() | client.list_resource_templates() | resources/templates/list | 查询 URI Template |
@mcp.resource() | client.read_resource() | resources/read | 读取资源 |
@mcp.prompt() | client.list_prompts() | prompts/list | 查询 Prompt |
@mcp.prompt() | client.get_prompt() | prompts/get | 渲染 Prompt |
连接进入 async with 后,Client 还会提供:
client.server_info
client.server_capabilities
client.protocol_version
client.instructions它们分别表示 Server 自报身份、能力、双方使用的协议版本和 Server 说明。server_info 只是自报信息,不能拿它替代身份认证。
列表类 API 可能分页。正式 Client 应持续传入 cursor,直到结果中的 next_cursor 为 None,不能默认所有 Server 都一次返回完整列表。
11. SDK 背后实际发送了什么
理解底层报文有助于排查问题,但业务代码不需要手写它们。
Client 执行:
async def list_server_tools(client):
return await client.list_tools()SDK 会发送类似下面的 JSON-RPC request:
{
"jsonrpc": "2.0",
"id": "list-1",
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "study-ide",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}stdio 模式把它写入 Server 的 stdin;Streamable HTTP 模式给它套上 HTTP 请求头:
POST /mcp HTTP/1.1
Host: mcp.example.com
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/list
Content-Type: application/json
Accept: application/json, text/event-stream
{ "jsonrpc": "2.0", "id": "list-1", "method": "tools/list", "params": { ... } }tools/list 没有具体工具名,所以不需要 Mcp-Name。这类请求可以由官方 Client、其他语言 SDK、Postman 或 curl 发出;Server 返回工具定义后,Python SDK 会将结果转换成 ListToolsResult。
Client 执行:
async def search_timeout(client):
return await client.call_tool(
"search_project",
{"query": "PAYMENT_TIMEOUT", "limit": 5},
)底层请求的方法变成 tools/call:
{
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/call",
"params": {
"name": "search_project",
"arguments": {
"query": "PAYMENT_TIMEOUT",
"limit": 5
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "study-ide",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}HTTP 请求大致如下:
POST /mcp HTTP/1.1
Host: mcp.example.com
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search_project
Content-Type: application/json
Accept: application/json, text/event-stream
{ "jsonrpc": "2.0", "id": "call-1", "method": "tools/call", "params": { ... } }Mcp-Method 必须与 body 中的 method 一致。对于 tools/call、resources/read 和 prompts/get,还要发送 Mcp-Name,它分别对应工具名、资源 URI 或 Prompt 名称。Header 与 body 不一致时,最新版 Server 应返回 400 Bad Request 和 HeaderMismatch。
短请求可以直接返回 JSON;需要在同一个请求中发送过程事件时,Server 可以返回 text/event-stream。它们仍然使用同一个 MCP endpoint,并不是两套业务 API。
12. 把模型调用转换成 MCP 调用
Host 从 client.list_tools() 得到工具定义后,会把它们转换成模型 API 使用的 Function Calling 格式:
MCP Tool.name → 模型函数名
MCP Tool.description → 模型函数描述
MCP Tool.inputSchema → 模型函数参数 Schema模型可能返回:
{
"name": "search_project",
"arguments": {
"query": "PAYMENT_TIMEOUT",
"limit": 5
}
}这条消息属于模型 API,还不是 MCP 请求。Host 验证参数与权限后,才调用:
async def execute_model_call(client, arguments):
return await client.call_tool("search_project", arguments)Server 的结果经过 Host 裁剪和脱敏,再作为 tool result 放回 Tool Loop。模型可以根据搜索结果直接回答,也可以继续读取命中的 Resource。
模型提出 Function Calling
↓
Host 校验参数、权限和用户确认
↓
MCP Client 调用 client.call_tool()
↓
MCP Server 执行 @mcp.tool() 函数
↓
结果回到 Tool Loop13. 错误怎样返回
在 Tool 中主动抛出 ToolError:
raise ToolError("query 不能为空")Client 不会因此抛出普通 Python 异常,而会收到一个工具结果:
result.is_error # True
result.structured_content # None错误说明位于 result.content,模型可以读取并修改参数。调用不存在的工具通常也会得到 is_error=True 的结果。
如果 Server 返回 JSON-RPC error,Client 才会抛出 MCPError。这通常表示协议层问题,例如方法不支持或请求结构无效。业务代码应区分工具失败与协议失败,日志中也不要记录未脱敏的令牌、绝对路径和原始堆栈。
14. stdio 与 Streamable HTTP 怎么选
| 问题 | stdio | Streamable HTTP |
|---|---|---|
| Server 在哪里 | 与 Host 同一台机器 | 独立部署在网络中 |
| Client 怎样连接 | Client(StdioServerParameters(...)) | Client("https://.../mcp") |
| 消息怎样传输 | stdin / stdout | HTTP POST,响应为 JSON 或 SSE |
| 谁管理生命周期 | Host 启停子进程 | 服务平台管理进程与实例 |
| 常见认证方式 | Host 配置、环境和操作系统权限 | OAuth、Bearer Token、mTLS |
| 常见用途 | 文件、Git、本地开发工具 | 企业知识库、工单、共享服务 |
stdio 没有 /mcp URL,也不使用 HTTP OAuth。它的风险主要来自本地进程权限、环境变量和目录范围。
Streamable HTTP 要处理认证、Origin、TLS、租户、超时、限流和代理配置。Client 应使用 Server 提供的最终 URL;官方 SDK 只在同源范围内跟随有限重定向,不会把凭据带到另一个来源。
旧版 HTTP+SSE 怎样工作
2024-11-05 规范里的 HTTP+SSE transport 是旧版 MCP 的远程传输方式。它与最新版 Streamable HTTP 不是同一种连接方式。
旧版之所以同时使用 SSE 和 POST,是因为两个方向分开了:SSE 长连接负责 Server 向 Client 发消息,普通 HTTP POST 负责 Client 向 Server 发消息。
假设旧 Server 公布的地址是 https://legacy.example.com/sse,一次连接会这样进行:
1. Client → Server
GET /sse
建立一条长期保持的 SSE 连接
2. Server → Client,沿 SSE 连接发送
event: endpoint
data: /messages?session_id=abc123
3. Client → Server
POST /messages?session_id=abc123
body.method = "tools/list"
4. Server → Client,仍沿第 1 步的 SSE 连接发送
event: message
data: {"jsonrpc":"2.0","id":1,"result":{...}}这里的 endpoint 事件很关键。Client 不是自己猜一个 /messages 地址,而是等待 Server 告诉它后续应该 POST 到哪里。Server 返回的 URI 可能带有连接标识;Client 还要确认它没有跳到另一个来源,避免把认证信息发错地方。
调用 search_project 时也是同一条路线:请求通过 POST 发出去,执行结果通过最初建立的 SSE 连接回来。换句话说,旧版像是“寄件窗口”和“收件窗口”分开;最新版 Streamable HTTP 则把每次请求和响应放在同一个 POST 往返里。
官方 Python SDK v2 仍保留 sse_client(),用途也是兼容已有服务:
import anyio
from mcp import Client
from mcp.client.sse import sse_client
async def main() -> None:
transport = sse_client("https://legacy.example.com/sse")
async with Client(transport) as client:
listed = await client.list_tools()
print([tool.name for tool in listed.tools])
if __name__ == "__main__":
anyio.run(main)保留 Client 支持不等于应该继续创建新的 SSE Server。官方规范已在 2025-03-26 用 Streamable HTTP 取代 HTTP+SSE,并把后者列为 Deprecated。新旧方式可以用下面这张表快速判断:
| 对比项 | 旧版 HTTP+SSE | 2026-07-28 Streamable HTTP |
|---|---|---|
| 连接入口 | Client 先 GET SSE endpoint | Client 直接 POST MCP endpoint |
| Client 发消息 | POST 到 endpoint 事件给出的地址 | 每个请求 POST 到同一个 MCP endpoint |
| Server 回消息 | 通过长期 SSE 连接发送 message 事件 | 当前 POST 直接返回 JSON,或在该响应内使用 SSE |
| 地址数量 | SSE 入口和消息 POST 地址分开 | 一个 MCP endpoint |
| 长期 GET 流 | 必需 | 已移除 |
| 新项目选择 | 不建议,只用于兼容旧 Server | 推荐的远程传输方式 |
最新版响应的 Content-Type 也可能是 text/event-stream,但这不代表它使用旧 HTTP+SSE transport。判断标准不是“有没有看到 SSE”,而是有没有先建立独立的 GET SSE 通道,再把请求 POST 到另一个地址。官方的弃用和兼容策略见 Streamable HTTP 的 Backward Compatibility。
15. 当前版与旧教程为什么不同
2026-07-28 Key Changes 调整了协议生命周期:
| 方面 | 2026-07-28 当前版 | 2025-11-25 及更早版本 |
|---|---|---|
| 初始化 | 无 initialize 握手 | initialize 后发送 notifications/initialized |
| 能力声明 | 每个 request 在 _meta 中自描述 | 主要在连接初始化时协商 |
| HTTP session | 无 Mcp-Session-Id | 部分版本依赖协议级 session |
| Server 发现 | 可调用 server/discover | 主要靠初始化和 list 方法 |
| Server 补充请求 | 使用 MRTR | Server 可以在连接上发起 Client request |
Python SDK v2 的 Client 会探测 Server,并在需要时兼容经典握手。应用通常不必自己拼版本回退逻辑,但依赖文件仍应锁定 SDK 版本。
Roots、Sampling 和 Logging 在当前规范中处于 Deprecated 状态,不等于已经删除。维护旧 Server 时仍可能遇到它们,新实现则不应继续增加依赖。状态以官方 Deprecated Features 为准。
16. 权限仍由 Host 和 Server 负责
MCP 统一了交互方式,没有替系统完成授权。
Host 决定哪些 Server 和工具可以交给模型,高风险动作是否需要用户确认,结果进入模型上下文前是否要裁剪和脱敏。Server 对每次调用检查身份、参数、目录、业务权限、响应大小和超时。
远程 Server 可以使用 MCP 的 OAuth 2.1 授权流程,具体见官方 Authorization。MCP Server 不能把 MCP Client 的 token 原样转发给第三方 API;下游凭据要单独取得,并绑定用户与目标服务。
工具描述、annotations、Resource、Prompt 和工具结果都属于外部输入。即使 Server 自称“只读”,Host 也要执行自己的权限规则。对于文件搜索服务,至少限制项目根目录、文件类型、单次返回数量和可读取的文件大小。