MCP 传输层:stdio 与 Streamable HTTP
搞清楚 MCP 的两种消息传输方式——本地用 stdio、远程用 Streamable HTTP,以及怎么选。
搞清楚 MCP Server 能暴露的三种能力——工具、资源、提示词模板,以及什么时候该用哪个。
内容摘要
如果说 MCP 架构讲的是"谁和谁连接",那原语(Primitives)讲的就是"连接之后能传递什么"。
如果说 MCP 架构讲的是"谁和谁连接",那原语(Primitives)讲的就是"连接之后能传递什么"。
MCP 把 Server 能提供的所有东西归成三类,官方叫它们原语——你可以理解成三块标准化的积木,所有 MCP Server 都是用这三块积木拼出来的:
| 原语 | 一句话 | 控制方 |
|---|---|---|
| Tools(工具) | 让模型"做事"——执行函数、调 API、写文件 | 模型控制(model-controlled):模型自己决定要不要调 |
| Resources(资源) | 让模型"读数据"——文件内容、数据库表结构、API 返回体 | 应用控制(application-controlled):Host 决定怎么塞进上下文 |
| Prompts(提示词模板) | 让用户"选模板"——预定义好的提示词,用户手动触发 | 用户控制(user-controlled):用户在 UI 上选择调用 |
三者的核心区别在于谁来触发:Tools 由模型自主决定调用,Resources 由应用程序自动管理,Prompts 由用户手动选取。
用一个类比来记:Tools 是菜刀(动手做菜),Resources 是食材(只读、提供原料),Prompts 是菜谱(预设好的操作模板,用户翻开才用)。
不确定该用哪种原语?按这个思路判断:
Tools 是最常用的原语。当模型在对话中判断"我需要调用一个外部功能"时,它从 Host 汇总的工具列表里选一个 Tool,发起调用。
发现工具——Client 先问 Server 有哪些工具可用:
// 请求
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
// 响应
{
"jsonrpc": "2.0", "id": 1,
"result": {
"tools": [{
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "City name or zip code" }
},
"required": ["location"]
}
}]
}
}
调用工具——模型选好工具和参数,Client 发起调用:
// 请求
{
"jsonrpc": "2.0", "id": 2,
"method": "tools/call",
"params": { "name": "get_weather", "arguments": { "location": "Beijing" } }
}
// 响应
{
"jsonrpc": "2.0", "id": 2,
"result": {
"content": [{ "type": "text", "text": "北京当前:28°C,晴,东风 3 级" }],
"isError": false
}
}
几个关键细节:
inputSchema:用 JSON Schema 描述参数格式,模型会自动按这个格式填参数isError:如果工具执行出错(比如 API 限流),Server 在结果里把 isError 设成 true,而不是抛 JSON-RPC 错误。这样模型可以"看到"错误信息并自行决定如何处理text)、图片(image)、音频(audio)、资源链接(resource_link)或嵌入资源(resource)outputSchema(可选):Server 可以声明工具返回的结构化数据格式,方便 Client 做校验notifications/tools/list_changed 通知,Client 就会重新拉取Resources 是"只读数据提供者"。和 Tools 的最大区别:Resources 不产生副作用,它只是把数据暴露给 Host,由 Host 或应用程序决定是否把这些数据塞进模型的上下文窗口。
发现资源:
// 请求
{ "jsonrpc": "2.0", "id": 1, "method": "resources/list" }
// 响应
{
"jsonrpc": "2.0", "id": 1,
"result": {
"resources": [{
"uri": "file:///project/src/main.rs",
"name": "main.rs",
"title": "Rust Application Main File",
"description": "Primary application entry point",
"mimeType": "text/x-rust"
}]
}
}
读取资源:
// 请求
{
"jsonrpc": "2.0", "id": 2,
"method": "resources/read",
"params": { "uri": "file:///project/src/main.rs" }
}
// 响应
{
"jsonrpc": "2.0", "id": 2,
"result": {
"contents": [{
"uri": "file:///project/src/main.rs",
"mimeType": "text/x-rust",
"text": "fn main() {\n println!(\"Hello world!\");\n}"
}]
}
}
几个关键细节:
file://、https://、git:// 和自定义 schemefile:///{path}),Client 填入参数后读取具体资源resources/subscribe 订阅某个资源,当资源内容变化时 Server 发通知text 字段)或二进制(blob 字段,base64 编码)audience(给用户还是给模型看)、priority(重要程度 0-1)、lastModified(最后修改时间)等元数据Prompts 是"预设好的提示词模板",用户在 UI 上手动选择触发。最典型的场景:在对话框里输入 /code_review,然后粘贴一段代码,Server 会返回一套完整的"代码评审提示词"塞进对话。
列出可用模板:
// 请求
{ "jsonrpc": "2.0", "id": 1, "method": "prompts/list" }
// 响应
{
"jsonrpc": "2.0", "id": 1,
"result": {
"prompts": [{
"name": "code_review",
"title": "Request Code Review",
"description": "Asks the LLM to analyze code quality and suggest improvements",
"arguments": [
{ "name": "code", "description": "The code to review", "required": true }
]
}]
}
}
获取模板内容(传入参数):
// 请求
{
"jsonrpc": "2.0", "id": 2,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": { "code": "def hello():\n print('world')" }
}
}
// 响应
{
"jsonrpc": "2.0", "id": 2,
"result": {
"description": "Code review prompt",
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": "Please review this Python code:\ndef hello():\n print('world')"
}
}]
}
}
关键细节:
role: "user" 和 role: "assistant" 交替),不只是一条文本arguments,用户填入参数后 Server 动态生成对应的消息除了上面三种服务端原语,MCP 还定义了三种客户端原语——由 Client 暴露给 Server 调用:
| 客户端原语 | 作用 | 使用场景 |
|---|---|---|
| Sampling | Server 请求 Client 的大模型生成一段回复 | Server 自己不想集成 LLM SDK,借用 Host 的模型能力 |
| Elicitation | Server 请求 Client 向用户提问并等待回答 | 执行敏感操作前要求用户确认 |
| Logging | Server 向 Client 发送日志消息 | 调试、审计、进度显示 |
这三个初学者暂时不用深入,知道有这回事就好。
理解三大原语最好的办法是看一个同时用了三种原语的 Server。下面用 Python SDK 写一个"项目助手" Server,它同时提供 Tool、Resource 和 Prompt:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("project-assistant")
# ---------- Tool:执行代码格式化(有副作用) ----------
@mcp.tool()
def format_code(code: str, language: str = "python") -> str:
"""Format code using standard style guidelines"""
# 实际实现里会调用 black / prettier 等工具
return f"Formatted {language} code:\n{code.strip()}"
# ---------- Resource:提供项目 README(只读) ----------
@mcp.resource("file:///project/README.md")
def get_readme() -> str:
"""Project README file"""
with open("/project/README.md") as f:
return f.read()
# ---------- Prompt:代码评审模板(用户手动触发) ----------
@mcp.prompt()
def code_review(code: str) -> str:
"""Ask the LLM to review code quality"""
return f"""Please review the following code for:
1. Code quality and readability
2. Potential bugs
3. Performance issues
4. Security concerns
Code:
{code}"""
if __name__ == "__main__":
mcp.run()
启动这个 Server 后:
tools/list 发现 format_code 这个工具,在需要格式化代码时自动调用resources/list 发现 file:///project/README.md,可以自动把 README 内容塞进上下文/code_review,选择这个 Prompt 模板,粘贴代码后 Server 返回一套完整的评审提示词| 维度 | MCP Tools | MCP Resources | MCP Prompts | OpenAI Function Calling | LangChain Tools |
|---|---|---|---|---|---|
| 控制方 | 模型自主决定 | 应用程序 | 用户手动 | 模型自主决定 | 模型自主决定 |
| 有无副作用 | 有 | 无(只读) | 无 | 有 | 有 |
| 返回格式 | content 数组(多类型) | URI + 文本/二进制 | 完整消息数组 | JSON 字符串 | Python 对象 |
| 动态发现 | ✅ tools/list + 变更通知 | ✅ resources/list + 订阅 | ✅ prompts/list | ❌ 写死在 API 调用里 | ❌ 写死在代码里 |
| 跨客户端复用 | ✅ | ✅ | ✅ | ❌ 只在 OpenAI 生态 | ❌ 只在 LangChain |
| 参数校验 | JSON Schema | URI + MIME | arguments 声明 | JSON Schema | Python 类型标注 |
核心区别:
| 误区 | 准确理解 |
|---|---|
| 以为 Resources 就是"只读版 Tool" | Resources 根本不是给模型"调用"的。Resources 是给 Host 用来填充上下文的数据源,由应用程序自动决定用不用;Tool 是给模型主动调用的 |
| 以为 Prompts 是 System Prompt | Prompts 是用户手动选择的消息模板,可以包含多轮对话和嵌入资源,和 System Prompt 是完全不同的概念 |
| 以为一个 Server 必须三种原语都提供 | 完全不必。绝大多数 Server 只暴露 Tools,少数加上 Resources,Prompts 更少。只提供其中一种也完全合法 |
以为 Tool 的 isError: true 等于 JSON-RPC 错误 | 不一样。JSON-RPC 错误是协议层面的错误(比如方法名不存在),isError: true 是工具执行层面的错误(比如 API 限流),模型可以看到后者的错误信息并尝试修复 |
| 以为 Resources 不能动态更新 | Resources 支持 subscribe + notifications/resources/updated 机制,Client 可以订阅某个资源,变化时自动收到通知并重新读取 |
| 优势 | 劣势 |
|---|---|
| 职责分离清晰:把"做事"、"读数据"、"用模板"分成三种原语,每种有明确的语义和控制方,避免所有东西都叫"Tool"导致的混乱 | 概念门槛:新手需要理解三种原语的区别,比起 Function Calling 的"只有 Tool"更复杂 |
动态发现 + 变更通知:三种原语都支持 list + listChanged 通知,工具和数据可以运行时增减,不用重启 | Prompts 支持参差:不是所有 Host 都实现了 Prompts 功能,比如某些客户端只支持 Tools |
| Resources 的只读语义:明确告诉 Host "这个数据没有副作用",Host 可以放心地自动把它塞进上下文而不需要用户确认 | Resources 在实际使用中不如 Tools 直观:很多开发者倾向于把所有东西都写成 Tool,因为 Tool 的"调用"心智模型更简单 |
| Prompts 标准化了提示词分发:Server 作者可以把精心调优的提示词打包分发,用户不需要自己写 Prompt | 三原语的边界有时模糊:比如"读取文件内容"到底该用 Tool 还是 Resource?需要具体分析是否有副作用 |
参考答案:
取决于具体场景:
inputSchema 可以描述路径参数。实际上官方的 Filesystem Server 把 read_file 实现为 Tool(因为模型需要指定读哪个文件),同时可以把项目根目录的结构暴露为 Resource(方便 Host 自动加载目录树作为上下文)。两种原语可以在同一个 Server 里互补使用。
参考答案:
两种错误的处理链路完全不同:
协议层错误(JSON-RPC error)表示请求本身有问题——工具名不存在、参数格式不对、Server 内部崩溃。这种错误由 Client/Host 在协议层面处理,模型看不到也不需要看到,因为没有有意义的信息可以让模型修正。
工具执行错误(isError: true)表示工具跑了、但业务逻辑出了问题——API 限流了、权限不够、输入数据不合法。这种错误需要让模型看到,因为模型有可能根据错误信息调整策略(换个参数重试、告诉用户原因、改用别的工具)。
如果把这两种错误混在一起(像传统 RPC 那样只用错误码),模型就没法区分"这个工具根本不存在"和"这个工具存在但这次调用限流了"。前者应该放弃,后者可以等一下重试。
参考答案:
一个合理的设计:
Tools(模型控制,有副作用):
execute_query:执行 SQL 查询(SELECT / INSERT / UPDATE / DELETE)create_table:创建新表alter_table:修改表结构Resources(应用控制,只读):
db://schema:整个数据库的表结构概览(Host 启动时自动加载到上下文,让模型知道有哪些表和字段)db://tables/{table_name}/schema:用 Resource Template 暴露每张表的详细 schema(Host 按需读取)db://tables/{table_name}/sample:每张表的前 10 行示例数据Prompts(用户控制,模板):
sql_optimization:用户手动触发,粘贴一条慢 SQL,Server 返回一套"SQL 性能优化分析"的提示词模板data_migration:用户描述需求后,Server 返回数据迁移方案的提示词这个分配的逻辑是:需要"做动作"的用 Tool,需要"提供背景信息"的用 Resource,需要"专业提示词模板"的用 Prompt。Resource 暴露的 schema 信息会被 Host 自动塞进上下文,这样模型在写 SQL 时就已经"知道"表结构了,不需要每次都先调 Tool 查 schema。
优先展示同分类且标签更接近的内容,方便继续串联学习。
搞清楚 MCP 的两种消息传输方式——本地用 stdio、远程用 Streamable HTTP,以及怎么选。
搞清楚 MCP 里 Host、Client、Server 各自干什么、怎么连接、消息怎么流转。
MCP 的关键不是“连上就能用”,而是先走严格生命周期:版本协商、能力交换、initialized 通知,然后才能进入正常通信。