Agent 工程体系 · 第 38/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

MCP Server 工程:能力注册、Context、并发、错误和部署

MCP Server 是一个通过 Model Context Protocol 向 Agent Host 提供能力的服务端程序。这里的“能力”不是一个笼统的函数集合,而是由协议明确建模的三类原语:

  • Resources:供用户或模型读取的上下文与数据;
  • Prompts:可复用的提示模板和工作流;
  • Tools:由模型请求、由服务器执行的函数。

MCP 使用 JSON-RPC 2.0 作为消息格式。架构上,Host 管理多个 Client,每个 Client 与一个 Server 保持一对一关系;Server 只接收当前请求提供的必要信息,不应直接看到 Host 的完整对话,也不能跨 Server 读取上下文。(modelcontextprotocol.io)

本文以 2026-07-28 协议版本作为 2026 年 9 月的工程基线。这个版本的重要变化是:协议核心不再依赖 initialize 握手和 Mcp-Session-Id 会话,客户端能力、协议版本等信息改为随每个请求携带;HTTP Server 可以因此按普通无状态服务部署。旧版本仍可能存在于兼容场景中,不能把旧版会话语义和新版无状态语义混用。(modelcontextprotocol.io)

一、先建立正确的 Server 心智模型

一个 MCP Server 可以抽象为:

S=(R,P,T,E,D)S = (R, P, T, E, D)

其中:

  • RR:Resource 注册表;
  • PP:Prompt 注册表;
  • TT:Tool 注册表;
  • EE:执行器,负责参数校验、授权、调用业务逻辑和结果编码;
  • DD:发现与能力描述信息。

一次请求可以写成:

execute(m,p,c)(r,e)\operatorname{execute}(m, p, c) \rightarrow (r, e)

  • mm:MCP 方法,例如 tools/call
  • pp:请求参数;
  • cc:当前请求的 Context;
  • rr:协议结果;
  • ee:协议错误或执行错误。

这里的 Context 不是“当前聊天记录”。在 MCP Server 内部,Context 更接近:

Context =
    当前请求的元数据
  + 客户端声明的能力
  + 协议版本
  + 请求取消信号
  + 日志与追踪信息
  + 认证主体
  + 服务器自身的业务依赖

在 2026-07-28 版本中,客户端必须在每个请求的 _meta 中携带协议版本和客户端能力。服务器不能因为同一连接之前出现过某个能力声明,就在后续请求中默认该能力仍然存在。需要跨请求保存的业务状态,也必须通过显式句柄传递,而不能依赖连接或进程身份。(modelcontextprotocol.io)

因此,下面两种设计的语义完全不同:

错误:connection -> current_user -> current_cart
正确:create_cart() -> cart_id
      add_item(cart_id, item)

第一种把连接误当成业务会话;第二种把业务状态显式化,任何服务器实例都可以根据 cart_id 找到对应状态。

二、能力注册:注册表不是装饰信息

2.1 能力注册解决什么问题

Agent 在调用 Tool 之前必须知道至少四件事:

  1. Tool 的名称;
  2. Tool 的用途;
  3. 输入参数的结构;
  4. 结果如何解释。

因此,注册信息既是机器可读的接口描述,也是模型进行工具选择时的重要上下文。

一个 Tool 的最小抽象可以表示为:

{
  "name": "search_orders",
  "title": "Search Orders",
  "description": "Search orders belonging to the authenticated user",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "minLength": 1
      },
      "limit": {
        "type": "integer",
        "minimum": 1,
        "maximum": 50,
        "default": 20
      }
    },
    "required": ["query"],
    "additionalProperties": false
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "items": {
        "type": "array"
      },
      "nextCursor": {
        "type": ["string", "null"]
      }
    },
    "required": ["items", "nextCursor"],
    "additionalProperties": false
  }
}

这里有三个容易混淆的层次:

  • name 是协议调用时使用的稳定标识;
  • description 是给客户端、模型和开发者理解用途的说明;
  • inputSchemaoutputSchema 才是可验证的接口约束。

Schema 默认使用 JSON Schema 2020-12。实现必须支持无显式 $schema 时的 2020-12 方言,并且必须按照声明的方言校验 Schema。对外部 $ref 的自动网络解析默认必须关闭,否则恶意 Schema 可能诱导服务器访问内网地址或下载超大内容。(modelcontextprotocol.io)

2.2 server/discovertools/list 的区别

2026-07-28 协议中,Server 必须实现 server/discover。它用于一次性返回:

  • Server 支持的协议版本;
  • Server 能力;
  • Server 身份;
  • 可选的使用说明;
  • 可缓存信息。

例如:

{
  "jsonrpc": "2.0",
  "id": "discover-1",
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": {
      "tools": {},
      "resources": {},
      "prompts": {}
    },
    "_meta": {
      "io.modelcontextprotocol/serverInfo": {
        "name": "order-server",
        "version": "2.4.0"
      }
    },
    "instructions": "Use search_orders for lookup and get_order for a single order.",
    "ttlMs": 3600000,
    "cacheScope": "public"
  }
}

server/discover能力总览tools/listresources/listprompts/list具体注册项列表。客户端可以不调用 server/discover,而是直接发送其他请求并处理版本或能力错误;但在需要展示 Server 信息、预热能力缓存或兼容 stdio 旧实现时,显式发现更合适。(modelcontextprotocol.io)

能力声明的因果链如下:

sequenceDiagram
    participant H as Host
    participant C as MCP Client
    participant S as MCP Server
    participant B as 业务依赖

    H->>C: 创建 Client
    C->>S: server/discover
    S-->>C: supportedVersions + capabilities + ttlMs
    C->>S: tools/list
    S-->>C: Tool definitions
    H->>C: 请求 Agent 执行工具
    C->>S: tools/call + arguments + per-request _meta
    S->>S: 校验、授权、限流、执行
    S->>B: 查询或写入业务系统
    B-->>S: 业务结果
    S-->>C: CallToolResult
    C-->>H: 结构化结果或错误

关键边界是:注册能力不等于授权能力

一个 Tool 出现在 tools/list 中,只说明服务器能够提供它;它不说明当前用户一定有权限调用它。授权必须在每一次执行路径中再次检查,因为用户身份、租户、资源对象和风险等级都可能随请求变化。

2.3 注册表必须保持稳定

Tool 名称一旦被客户端缓存、被模型记忆或被工作流持久化,就成为兼容性表面。工程上应避免:

同一个 name 在不同版本中改变参数语义
同一个 name 有时返回 object、有时返回 array
description 说“只读”,实现却执行写操作
删除 Tool 后没有版本或迁移策略

更稳妥的做法是:

search_orders.v2
get_order
cancel_order

或者保持名称不变,只在 Schema 中采用向后兼容的可选字段。但无论采用哪种策略,name、输入 Schema、输出 Schema 和副作用语义都应作为接口版本的一部分进行测试。

三、Context:请求上下文、模型上下文和业务上下文不是一回事

3.1 三种 Context

MCP Server 工程中至少要区分三种 Context。

1. 协议请求 Context

它表示当前 MCP 请求携带的信息,例如:

{
  "_meta": {
    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
    "io.modelcontextprotocol/clientInfo": {
      "name": "agent-host",
      "version": "5.1.0"
    },
    "io.modelcontextprotocol/clientCapabilities": {
      "elicitation": {}
    },
    "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
  }
}

服务器可以读取这些信息来决定协议处理方式、记录日志和传播追踪上下文,但不能把 clientInfo 当作安全凭证。协议明确把客户端和服务器身份视为自报信息,不能用于安全决策。(modelcontextprotocol.io)

2. 模型上下文

这是 Host 准备给模型的内容,例如:

用户问题
+ 选中的 Resource 内容
+ Prompt 模板展开结果
+ 可用 Tool 定义
+ 之前的工具调用结果

模型上下文通常由 Host 组装,不应由 Server 假定自己能够看到。Server 只提供 Resource、Prompt 或 Tool 结果,不直接拥有完整会话。

3. 业务上下文

它表示执行 Tool 所需的真实业务信息:

tenant_id
subject_id
authorization scopes
request_id
database transaction
downstream timeout
idempotency key

业务上下文不应直接从模型参数中“猜出来”。例如,模型传入:

{
  "order_id": "o-1001"
}

这只能说明目标订单,不能说明调用者拥有该订单。tenant_id、用户身份和权限应来自认证层或 Host 传入的受信任上下文。

3.2 _meta 的正确用法

_meta 是 MCP 用于附加元数据的字段。协议保留了若干命名空间,例如:

  • io.modelcontextprotocol/protocolVersion
  • io.modelcontextprotocol/clientInfo
  • io.modelcontextprotocol/clientCapabilities
  • io.modelcontextprotocol/logLevel
  • traceparent
  • tracestate
  • baggage

第三方元数据应使用自己的前缀,推荐采用反向域名形式,例如:

{
  "_meta": {
    "com.example/requestId": "req-123",
    "com.example/tenant": "tenant-a"
  }
}

但是,客户端传入的普通 _meta 字段也不能自动视为可信身份。一个请求的可信身份来源应是 HTTP 认证、stdio 启动环境、Host 的本地进程边界或其他明确的认证机制,而不是任意可伪造的 JSON 字段。

四、Resources、Prompts 和 Tools 的边界

4.1 Resource 是“被读取的上下文”

Resource 描述数据,而不是动作。它可以是文本或二进制内容:

{
  "uri": "file:///project/README.md",
  "mimeType": "text/markdown",
  "text": "# Project"
}

二进制 Resource 使用 Base64:

{
  "uri": "file:///project/logo.png",
  "mimeType": "image/png",
  "blob": "iVBORw0KGgo..."
}

Resource 还可以带 audienceprioritylastModified 注解。客户端可以据此判断资源更适合用户还是模型、是否应优先注入上下文,以及是否需要重新读取。(modelcontextprotocol.io)

一个常见错误是把所有内容都设计成 Tool:

错误:read_project_file(path)
正确:Resource: file:///project/README.md

如果操作本质上只是读取稳定内容,Resource 通常比 Tool 更容易缓存、展示和审计。反过来,如果读取过程需要动态权限判断、复杂参数或副作用,就应考虑 Tool。

4.2 Prompt 是模板,不是执行结果

Prompt 用于提供参数化的消息模板。例如:

Prompt: review_pull_request
参数:
  repository
  pull_request_number

返回:
  system message
  user message
  可选 Resource 引用

Prompt 的职责是帮助 Host 形成模型输入,不应把数据库写入、支付或删除操作隐藏在 Prompt 展开过程中。副作用应由 Tool 明确承担,这样用户确认、授权和审计才能落在可识别的执行点上。

4.3 Tool 是可执行能力

Tool 可能执行任意代码、访问数据库或调用外部服务,因此应被视为高风险边界。MCP 的安全原则要求服务器校验所有 Tool 输入、执行访问控制、进行限流并清理输出;Host 则应在敏感操作前要求用户确认。(modelcontextprotocol.io)

Tool 的执行过程不应只有:

解析参数 -> 调函数 -> 返回结果

而应至少包含:

解析 JSON
-> Schema 校验
-> 身份与权限校验
-> 资源级授权
-> 限流与配额
-> 幂等检查
-> 调用业务逻辑
-> 结果 Schema 校验
-> 脱敏与输出编码
-> 记录审计

五、并发:JSON-RPC 可并行,不代表业务逻辑天然安全

5.1 请求 ID 是并发关联的基础

每个 JSON-RPC 请求必须有唯一 ID。未收到响应前,发送方不能重复使用相同 ID;响应必须带回对应 ID。通知没有 ID,也不能产生响应。(modelcontextprotocol.io)

例如,客户端同时发送两个请求:

{"jsonrpc":"2.0","id":101,"method":"tools/call","params":{"name":"get_order","arguments":{"order_id":"o-1"}}}
{"jsonrpc":"2.0","id":102,"method":"tools/call","params":{"name":"get_order","arguments":{"order_id":"o-2"}}}

服务器可以先返回 id:102

{"jsonrpc":"2.0","id":102,"result":{"resultType":"complete","content":[{"type":"text","text":"order o-2"}]}}

再返回 id:101。客户端必须按 ID 关联,而不能按发送顺序关联。

5.2 协议无状态不等于执行无状态

“无状态”只表示服务器不能依赖隐含的协议会话状态。它不意味着每个 Tool 都没有内存、数据库或缓存。

可以区分:

状态类型 是否允许 正确做法
协议版本 不应隐式保存 每次从请求元数据读取
客户端能力 不应隐式保存 每次根据声明判断
Tool 注册表 可以保存 进程启动时构建,只读共享
数据库连接池 可以保存 进程级资源,受并发限制
购物车 可以保存 通过 basket_id 显式引用
当前用户 不应从连接猜测 从认证上下文获得
单次请求缓存 可以保存 绑定当前请求生命周期

5.3 并发限制应位于业务边界

服务器至少需要三类并发控制:

Ceffective=min(Cglobal,Ctool,Cdependency)C_{\text{effective}} = \min(C_{\text{global}}, C_{\text{tool}}, C_{\text{dependency}})

  • CglobalC_{\text{global}}:整个实例允许处理的最大请求数;
  • CtoolC_{\text{tool}}:某个高成本 Tool 的并发上限;
  • CdependencyC_{\text{dependency}}:数据库、第三方 API 或浏览器池的并发上限。

例如:

全局并发:100
search_orders:50
export_orders:4
数据库连接池:30

真正的有效并发不是 100,而是受各个依赖瓶颈限制。若 export_orders 每次占用一个数据库连接和一个临时文件句柄,把它放进全局无限线程池会导致请求排队、连接耗尽和内存增长。

5.4 反例:隐式可变状态导致交叉污染

current_user = None

async def call_tool(request):
    global current_user
    current_user = request.user
    await asyncio.sleep(0.1)
    return await query_orders(current_user)

并发执行时:

请求 A 设置 current_user = Alice
请求 B 设置 current_user = Bob
请求 A 恢复执行,读取到 Bob

结果是 Alice 可能获得 Bob 的订单。这不是“异步框架的问题”,而是把请求上下文放入共享可变变量造成的竞态。

应改为显式传递:

async def call_tool(request):
    user = request.user
    await asyncio.sleep(0.1)
    return await query_orders(user)

或者使用框架提供的 request-scoped context,但仍要确认它不会跨任务泄漏。

5.5 同一业务资源的并发更新

假设两个 Agent 同时调用:

update_order_status(order_id="o-1", status="cancelled")
update_order_status(order_id="o-1", status="shipped")

仅依赖 Tool 调用顺序是不安全的。服务器需要在业务层定义并发规则,例如:

UPDATE orders
SET status = :new_status,
    version = version + 1
WHERE id = :order_id
  AND version = :expected_version
  AND status IN ('paid', 'processing');

若更新行数为 0,则返回业务错误,而不是假装成功。MCP 负责传递调用和结果,不能替代数据库事务、乐观锁或状态机。

六、错误:协议错误和 Tool 执行错误必须分开

6.1 两个错误层次

MCP 至少有两种错误:

协议错误

表示请求本身无法按协议处理,例如:

  • JSON 无法解析;
  • JSON-RPC 结构非法;
  • 方法不存在;
  • 参数结构不符合协议;
  • 请求要求未声明的客户端能力;
  • 协议版本不支持。

协议错误通过 JSON-RPC 的 error 字段返回:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "field": "arguments.limit",
      "reason": "must be <= 50"
    }
  }
}

2026-07-28 版本使用标准 JSON-RPC 错误码,并为 MCP 定义了部分 -32020-32099 范围的错误。实现不得随意占用 MCP 保留范围;应用自定义错误应放在协议保留范围之外。(modelcontextprotocol.io)

Tool 执行错误

表示请求结构合法、Tool 找到了,但业务执行失败,例如:

  • 订单不存在;
  • 日期格式正确但早于当前日期;
  • 下游 API 返回拒绝;
  • 用户没有访问该订单的权限;
  • 业务状态不允许取消。

这类错误仍然是一个成功返回的 JSON-RPC result,但结果中的 isErrortrue

{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "resultType": "complete",
    "content": [
      {
        "type": "text",
        "text": "Order o-1 cannot be cancelled because it has already shipped."
      }
    ],
    "isError": true
  }
}

协议建议客户端把 Tool 执行错误提供给模型,因为模型可能据此修正参数或选择其他路径;协议错误通常说明调用方式本身不正确,恢复概率较低。(modelcontextprotocol.io)

6.2 错误分类的推导

可以用以下判定过程:

请求能否被 JSON 解析?
  否 -> JSON-RPC Parse Error

是否具有合法 JSON-RPC 结构和 ID?
  否 -> Invalid Request

method 是否存在?
  否 -> Method Not Found

params 是否符合协议和 Tool inputSchema?
  否 -> Invalid Params

当前调用是否被允许?
  否 -> Tool result: isError=true

业务执行是否成功?
  否 -> Tool result: isError=true

结果是否符合 outputSchema?
  否 -> 服务器内部错误或结果编码错误

全部通过
  -> resultType=complete, isError=false

不要把所有异常都直接抛成 JSON-RPC -32603。例如,“库存不足”不是协议故障,而是可解释的业务失败。把它标为 Tool 执行错误,模型才可能选择减少数量、查询替代商品或向用户说明原因。

6.3 错误结果必须可恢复

对 Agent 而言,错误消息不是日志,而是下一步决策输入。低质量错误:

failed

更好的错误:

Cannot cancel order o-1: current status is shipped.
Allowed statuses for cancellation: paid, processing.
No state was changed.

这里明确了:

  • 失败对象;
  • 当前状态;
  • 允许状态;
  • 是否产生副作用。

对于可能重试的下游错误,还应区分:

{
  "code": "UPSTREAM_TIMEOUT",
  "retryable": true,
  "retryAfterMs": 2000,
  "operationId": "op-123"
}

这属于应用层错误数据,不应伪装成 MCP 标准错误码。

七、结果编码:文本、结构化内容和输出 Schema

Tool 结果通常通过 content 返回。对于机器消费的结果,可以增加 structuredContent

{
  "resultType": "complete",
  "content": [
    {
      "type": "text",
      "text": "{\"items\":[{\"id\":\"o-1\",\"status\":\"paid\"}],\"nextCursor\":null}"
    }
  ],
  "structuredContent": {
    "items": [
      {
        "id": "o-1",
        "status": "paid"
      }
    ],
    "nextCursor": null
  }
}

如果声明了 outputSchema,服务器必须返回符合该 Schema 的 structuredContent,客户端应进行校验。为了兼容不理解结构化字段的客户端,提供结构化内容时也应同时返回序列化后的文本内容。structuredContent 是服务器结果数据,不等同于模型生成阶段的 Structured Output。(modelcontextprotocol.io)

输出校验的意义是防止“函数执行成功但协议结果不可用”:

业务函数返回 dict
-> outputSchema 校验失败
-> 不应把错误 dict 当成正常结果发给客户端

否则模型可能收到缺少字段、类型错误或混合结构,并在下一步产生错误推理。

八、取消、超时和长时间任务

8.1 取消必须传播到下游

请求超时不等于业务函数自动停止。服务器至少应有三层超时:

MCP 请求超时
  -> Tool 执行超时
      -> 下游 HTTP / SQL / 子进程超时

如果只在最外层设置超时:

await asyncio.wait_for(tool(), timeout=10)

而内部 HTTP 请求没有超时,任务可能仍然占用连接、线程或子进程资源。正确做法是将取消信号和截止时间传递给每一层依赖。

8.2 2026-07-28 HTTP 取消的变化

在 2026-07-28 的 Streamable HTTP 中,客户端取消正在进行的请求时,可以关闭该请求的 SSE 响应流作为取消信号;不再依赖旧式的 notifications/cancelled POST。旧版连接和 stdio 仍可能使用取消通知,因此兼容实现需要按协议时代处理两种行为。(ts.sdk.modelcontextprotocol.io)

服务器在收到取消后应做到:

停止等待下游结果
取消可取消的 HTTP/SQL/子进程操作
释放锁、连接、临时文件和并发槽位
不要再向已关闭的响应写入结果
记录取消原因和耗时

不能保证所有外部系统都支持真正取消。例如已经提交给支付网关的请求可能无法撤回。这时要依赖幂等键和状态查询,而不是把本地取消误认为远端回滚。

8.3 长任务不应靠无限挂起请求

如果导出任务需要数分钟,不应简单地让一个 HTTP 请求一直占用响应流。2026-07-28 的 Tasks 已作为扩展提供,用于异步执行、轮询和取消;它要求显式协商扩展能力。(modelcontextprotocol.io)

没有使用 Tasks 时,可以采用显式业务句柄:

start_export(format="csv")
-> export_id="exp-123"

get_export(export_id="exp-123")
-> status="running"

download_export(export_id="exp-123")
-> Resource

这种设计的关键是:export_id 必须有权限归属、过期时间、状态机和重试规则。

九、一个端到端的 Tool 设计示例

下面以 TypeScript SDK v2 风格展示核心结构。官方 SDK 页面列出的 Tier 1 SDK 包括 TypeScript、Python、C#、Go 和 Rust;各语言 API 遵循本语言习惯,但都支持构建 Server、暴露 Tools/Resources/Prompts,以及本地和远程传输。(modelcontextprotocol.io)

import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";

const server = new McpServer(
  {
    name: "order-server",
    version: "2.4.0",
  },
  {
    capabilities: {
      tools: {},
    },
  },
);

const input = z.object({
  orderId: z.string().min(1),
});

const output = z.object({
  orderId: z.string(),
  status: z.enum(["paid", "processing", "shipped", "cancelled"]),
  canCancel: z.boolean(),
});

server.registerTool(
  "get_order",
  {
    title: "Get Order",
    description: "Read one order visible to the authenticated caller.",
    inputSchema: input,
    outputSchema: output,
  },
  async ({ orderId }, ctx) => {
    const principal = ctx.auth?.principal;

    if (!principal) {
      return {
        content: [{ type: "text", text: "Authentication is required." }],
        isError: true,
      };
    }

    const order = await orderRepository.findVisibleOrder({
      orderId,
      subjectId: principal.subjectId,
      tenantId: principal.tenantId,
      signal: ctx.signal,
    });

    if (!order) {
      return {
        content: [{ type: "text", text: `Order ${orderId} was not found.` }],
        isError: true,
      };
    }

    const result = {
      orderId: order.id,
      status: order.status,
      canCancel: order.status === "paid" || order.status === "processing",
    };

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(result),
        },
      ],
      structuredContent: result,
    };
  },
);

这段代码的成立条件不是“SDK 能调用回调”,而是:

  1. 输入由 Schema 验证;
  2. 身份来自可信认证上下文,不来自 orderId
  3. 数据查询带租户和主体条件;
  4. 下游查询接收取消信号;
  5. 输出符合 outputSchema
  6. 业务失败使用 isError: true,而不是伪造协议错误。

实际项目中应根据所使用的 SDK 版本核对具体 import 路径和 Context 类型。协议字段和 SDK 公共 API 不是同一个稳定层次:协议规范定义线上数据格式,SDK 可能在不同主版本中调整注册方法、Context 结构和传输适配器。

十、部署:stdio 和 Streamable HTTP 是两种不同运行边界

10.1 stdio:本地进程边界

stdio 适合桌面 Host、CLI 或本地 Agent。Host 启动 MCP Server 子进程,通过:

stdin  -> JSON-RPC 请求
stdout -> JSON-RPC 响应
stderr -> 日志

因此,stdio Server 绝对不能把普通日志写到 stdout:

# 错误:会污染 JSON-RPC 流
print("processing request")

# 正确:写到 stderr
import logging
logging.basicConfig()
logging.info("processing request")

官方构建指南明确要求 stdio Server 使用 stderr 或日志文件记录日志;写入 stdout 会破坏 JSON-RPC 消息流。(modelcontextprotocol.io)

一个本地配置可以是:

{
  "mcpServers": {
    "order-server": {
      "command": "node",
      "args": ["/opt/mcp/order-server/dist/index.js"],
      "env": {
        "ORDER_DATABASE_URL": "postgresql://..."
      }
    }
  }
}

部署检查顺序应为:

确认 command 使用绝对路径
确认工作目录和环境变量
确认 stdout 只输出协议消息
确认 stderr 有启动和错误日志
确认子进程收到 SIGTERM 后能退出
确认数据库连接和临时文件被释放

stdio 的安全边界通常来自本机进程权限和 Host 控制。它不应照搬 HTTP OAuth 配置;协议规范对 HTTP 授权和 stdio 凭证获取采用不同建议。(modelcontextprotocol.io)

10.2 Streamable HTTP:远程服务边界

远程部署通常使用 Streamable HTTP。2026-07-28 的无状态模型下,HTTP 请求包含足够的协议元数据,可以被任意实例处理:

Client
  -> Load Balancer
      -> MCP instance A
      -> MCP instance B
      -> MCP instance C

不需要因为 MCP 协议会话而启用 sticky session 或共享会话存储。服务器如果有业务状态,必须将其放入数据库、对象存储、任务系统等外部持久化系统,并通过显式 ID 访问。(blog.modelcontextprotocol.io)

HTTP 请求示意:

POST /mcp HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_order
Authorization: Bearer <token>
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "get_order",
    "arguments": {
      "orderId": "o-1001"
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {
        "name": "agent-host",
        "version": "5.1.0"
      },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

Mcp-MethodMcp-Name 让网关、限流器和负载均衡器可以在不解析 JSON 正文的情况下进行路由和计量。服务器还应验证 Header 与 JSON-RPC body 是否一致,避免利用两者不一致绕过路由或审计。(blog.modelcontextprotocol.io)

10.3 HTTP 部署的最小拓扑

flowchart LR
    A[Agent Host] --> B[Auth Proxy]
    B --> C[Load Balancer]
    C --> D1[MCP Server 1]
    C --> D2[MCP Server 2]
    C --> D3[MCP Server 3]

    D1 --> E[(Database)]
    D2 --> E
    D3 --> E

    D1 --> F[Downstream API]
    D2 --> F
    D3 --> F

    D1 --> G[Logs / Traces]
    D2 --> G
    D3 --> G

这里的每个层次有不同职责:

  • Auth Proxy 验证令牌、提取主体和租户;
  • Load Balancer 按 HTTP 请求转发;
  • MCP Server 负责协议、注册、授权决策和 Tool 编排;
  • Database 保存需要跨请求的业务状态;
  • Downstream API 需要独立超时、重试和熔断;
  • Logs/Traces 用于按 request ID、Tool 名称和业务操作 ID诊断。

不要因为服务器是无状态的,就把鉴权移到 Tool 内部的任意字符串参数中。无状态解决的是路由和会话存储问题,不会自动解决身份、授权、数据隔离和密钥管理问题。

十一、缓存、版本和灰度发布

发现结果和列表结果可以携带 ttlMscacheScope。客户端据此决定缓存多久、缓存是否可跨用户共享。公共 Tool 列表可以使用公共缓存;包含租户特定能力的列表则必须使用用户或租户隔离缓存。(modelcontextprotocol.io)

缓存键至少应考虑:

server endpoint
authenticated tenant
protocol version
client capability profile
server release

以下缓存是危险的:

所有用户共享同一个 tools/list

如果 Tool 的可见性依赖用户权限,就可能出现:

管理员看到的 Tool 定义
被普通用户缓存并展示

即使普通用户最终调用仍会被拒绝,能力泄露和错误的模型规划也已经发生。

协议版本也应参与灰度策略。2026-07-28 使用每请求元数据;旧协议可能使用初始化握手。兼容 Server 可以同时接收两种时代的请求,但应用代码不能假设两者都具有相同的请求上下文和取消语义。TypeScript SDK 的迁移文档也明确区分了 HTTP 的无状态入口、stdio 的连接时代选择以及旧版取消方式。(ts.sdk.modelcontextprotocol.io)

十二、生产诊断:从线上的失败结果反推层次

症状 1:客户端提示“方法不存在”

检查:

是否注册了该 Tool
tools/list 是否包含该名称
Mcp-Name 是否与 body 中的 Tool 名一致
网关是否按旧名称路由
客户端是否缓存了过期能力列表

如果 tools/list 没有该 Tool,这是注册或版本问题;如果列表中有但调用失败,继续检查参数、授权和业务执行。

症状 2:stdio Server 启动后立刻断开

优先检查:

stdout 是否打印启动日志
是否有调试器写入 stdout
JSON 是否一行一个完整消息
command 是否使用绝对路径
子进程依赖和环境变量是否存在

stdio 协议流被一行普通文本污染时,客户端通常会表现为 JSON 解析失败,而不是清楚地提示“你的日志写错了”。

症状 3:HTTP Server 需要 sticky session 才能工作

这通常说明应用把以下某类状态错误地放在了进程内:

当前用户
当前会话
当前浏览器句柄
当前事务
当前任务

排查方式是抓取两次连续请求:

请求 A -> 实例 1
请求 B -> 实例 2

然后检查请求 B 是否携带了所有必要的显式 ID。若没有,应把状态改成:

业务句柄 + 外部存储

而不是重新启用隐式连接会话。

症状 4:模型不断重复调用同一个失败 Tool

检查 Tool 错误结果是否说明:

失败原因
是否发生副作用
是否可重试
重试需要改变什么参数
是否需要用户确认

isError: true 只是机器层面的失败标志,不足以帮助模型恢复。错误文本和结构化错误数据必须共同表达恢复路径。

症状 5:并发上升后数据库耗尽

按以下顺序定位:

MCP 请求并发
-> Tool 并发
-> 数据库连接池等待时间
-> 下游 API 并发
-> 单请求耗时分布

如果 MCP 全局并发是 100、数据库连接只有 20,而每个 Tool 都需要独占连接,那么剩余请求只是在内存中排队。应对高成本 Tool 设置独立信号量,并在超过队列预算时快速返回可恢复错误,而不是无限等待。

十三、Server 的工程验收条件

一个可部署的 MCP Server 至少应通过以下行为测试:

1. server/discover 返回正确的协议版本和能力
2. tools/list、resources/list、prompts/list 与实际注册表一致
3. 未知 Tool 返回明确错误
4. 错误参数不会进入业务函数
5. 未授权主体无法通过改变参数访问其他租户数据
6. 两个请求可以乱序完成并被正确关联
7. 同一业务对象的并发更新符合状态机规则
8. 客户端取消后,下游任务能够尽可能停止
9. Tool 业务失败使用 isError,而不是误报协议错误
10. structuredContent 始终符合 outputSchema
11. stdio stdout 不包含日志
12. HTTP 多实例之间不依赖隐式会话
13. 重启后显式业务句柄仍按设计可恢复或明确失效
14. 请求日志能够关联 request ID、Tool 名称和业务操作 ID

MCP Server 的核心不是“把几个函数暴露给模型”,而是把能力变成可发现、可验证、可授权、可取消、可并发执行和可部署的协议接口。能力注册决定客户端能否正确规划;Context 决定服务器是否在正确的安全边界内理解请求;并发模型决定多个 Agent 是否会互相污染;错误分类决定模型能否恢复;部署方式则决定这些语义能否在真实网络和多实例环境中保持成立。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。