Skip to content

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 再决定哪些能力可以交给模型或用户。

text
没有 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 CallingMCP
主要解决的问题模型怎样表达“我要调用哪个工具、参数是什么”Host 怎样发现、连接并调用外部 Server 的能力
交互双方模型 API 与应用MCP Client 与 MCP Server
工具定义从哪里来应用随模型请求传入Client 可通过 tools/list 从 Server 获取
是否规定连接方式不规定,应用自己执行函数或请求服务规定 stdio、Streamable HTTP 和协议消息
能力范围主要是供模型调用的函数工具Tools、Resources 和 Prompts
谁执行真实动作应用或它调用的后端MCP Server 中注册的函数或后端系统

两者经常一起工作,并不是二选一。文件搜索工具的一种常见接法是:

text
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 之间到底交换了什么。

text
用户
  ↓
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:

bash
uv add "mcp[cli]"

也可以使用 pip:

bash
python -m pip install "mcp[cli]"

安装后主要会用到这些对象:

python
from mcp import Client, StdioServerParameters
from mcp.server import MCPServer
  • MCPServer 用来创建 Server,并注册 Tool、Resource 和 Prompt;
  • Client 用来连接 Server;
  • StdioServerParameters 描述本地子进程怎样启动。

5. 用 MCPServer 创建文件搜索服务 ​

先创建 server.py:

python
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() 注册工具 ​

定义一下搜索结果的结构:

python
class Match(BaseModel):
    path: str
    line: int
    preview: str


class SearchResult(BaseModel):
    matches: list[Match]

再把普通 Python 函数注册成 Tool:

python
@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:

python
@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 需要一个用户主动选择的“审查文件”模板,可以继续添加:

python
@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 末尾加入:

python
if __name__ == "__main__":
    mcp.run()

直接运行时,mcp.run() 使用 stdio。调试阶段也可以交给官方 Inspector:

bash
uv run mcp dev server.py

6. 用官方 Client 通过 stdio 调用 ​

stdio 适合本地文件、Git 和开发工具。Client 启动 Server 子进程,通过 stdin/stdout 交换协议消息,退出上下文时再关闭子进程。

创建 client_stdio.py:

python
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:

bash
uv run client_stdio.py

async with Client(server) 开始时,SDK 启动 uv run server.py;退出时,SDK 关闭 stdin,等待子进程结束,必要时再终止它。业务代码不用自己管理 subprocess.Popen。

正常情况下会看到类似结果:

text
['search_project']
{'matches': [{'path': 'src/payment.py', 'line': 18, 'preview': 'PAYMENT_TIMEOUT = 15'}]}
False

content 主要给模型读取,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 的本地配置为例:

json
{
  "mcpServers": {
    "project-search": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/project-search-server",
        "run",
        "server.py"
      ],
      "env": {
        "PROJECT_ROOT": "/absolute/path/to/your/project"
      }
    }
  }
}

这几个字段分别表示:

字段作用
mcpServersHost 管理的 MCP Server 集合
project-search自己起的 Server 名称,会显示在 Host 中
commandHost 要启动的可执行程序
args传给命令的参数
env只传给该 Server 的环境变量

mcpServers 不是 MCP 协议消息,也不是所有 Host 必须使用的标准配置文件。它是许多 Host 采用的配置约定;不同 IDE 可能增加 cwd、disabled 或权限字段,实际字段要以对应 Host 文档为准。

Claude Desktop 的配置文件通常位于:

text
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 实例:

bash
uv run mcp run server.py --transport streamable-http

默认示例地址是:

text
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 默认值,不是协议保留字;需要挂在统一网关下面时,可以改路径:

python
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:

python
@mcp.tool()
def search_project(query: str) -> SearchResult:
    return search_files(query)

它做的是把 search_project 注册进 MCP 工具列表。一次完整调用是这样发生的:

  1. Client 向 MCP endpoint 发送 tools/list。
  2. Server 返回 search_project 的名称、说明和参数 Schema。
  3. Host 或模型决定调用它。
  4. Client 再发送 tools/call,并在 params.name 中填写 search_project。
  5. SDK 找到被 @mcp.tool() 装饰的 Python 函数,执行后把结果放回 MCP 响应。

前后两次请求都进入同一个 HTTP endpoint:

text
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:

python
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 管理:

python
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 添加远程地址,例如:

text
https://mcp.example.com/mcp

部分 IDE 和开发工具也允许把远程 Server 写进项目配置,常见形状如下:

json
{
  "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 还会提供:

python
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 执行:

python
async def list_server_tools(client):
    return await client.list_tools()

SDK 会发送类似下面的 JSON-RPC request:

json
{
  "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 请求头:

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 执行:

python
async def search_timeout(client):
    return await client.call_tool(
        "search_project",
        {"query": "PAYMENT_TIMEOUT", "limit": 5},
    )

底层请求的方法变成 tools/call:

json
{
  "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 请求大致如下:

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 格式:

text
MCP Tool.name         → 模型函数名
MCP Tool.description  → 模型函数描述
MCP Tool.inputSchema  → 模型函数参数 Schema

模型可能返回:

json
{
  "name": "search_project",
  "arguments": {
    "query": "PAYMENT_TIMEOUT",
    "limit": 5
  }
}

这条消息属于模型 API,还不是 MCP 请求。Host 验证参数与权限后,才调用:

python
async def execute_model_call(client, arguments):
    return await client.call_tool("search_project", arguments)

Server 的结果经过 Host 裁剪和脱敏,再作为 tool result 放回 Tool Loop。模型可以根据搜索结果直接回答,也可以继续读取命中的 Resource。

text
模型提出 Function Calling
          ↓
Host 校验参数、权限和用户确认
          ↓
MCP Client 调用 client.call_tool()
          ↓
MCP Server 执行 @mcp.tool() 函数
          ↓
结果回到 Tool Loop

13. 错误怎样返回 ​

在 Tool 中主动抛出 ToolError:

python
raise ToolError("query 不能为空")

Client 不会因此抛出普通 Python 异常,而会收到一个工具结果:

python
result.is_error          # True
result.structured_content  # None

错误说明位于 result.content,模型可以读取并修改参数。调用不存在的工具通常也会得到 is_error=True 的结果。

如果 Server 返回 JSON-RPC error,Client 才会抛出 MCPError。这通常表示协议层问题,例如方法不支持或请求结构无效。业务代码应区分工具失败与协议失败,日志中也不要记录未脱敏的令牌、绝对路径和原始堆栈。

14. stdio 与 Streamable HTTP 怎么选 ​

问题stdioStreamable HTTP
Server 在哪里与 Host 同一台机器独立部署在网络中
Client 怎样连接Client(StdioServerParameters(...))Client("https://.../mcp")
消息怎样传输stdin / stdoutHTTP 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,一次连接会这样进行:

text
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(),用途也是兼容已有服务:

python
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+SSE2026-07-28 Streamable HTTP
连接入口Client 先 GET SSE endpointClient 直接 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 补充请求使用 MRTRServer 可以在连接上发起 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 也要执行自己的权限规则。对于文件搜索服务,至少限制项目根目录、文件类型、单次返回数量和可读取的文件大小。

持续学习,持续实践。