MCP 变成无状态 HTTP 之后,边缘上终于可以管它了

·阅读约9分钟·Evergreen Tools Team

MCP 在大部分生命周期里都是有状态的。客户端发 initialize,服务端返回一个 Mcp-Session-Id,之后每个请求都要带着这个标识回到同一个服务实例。远程部署因此需要固定连接、在副本间共享会话状态,或者干脆架一层代理把流一直挂着。边缘检查根本不现实,因为请求流是不透明的。2026-07-28 版修订把会话整个去掉了,而正是这一处改动,让 MCP 端点可以用你早就对着 REST API 用的那套工具来管。

相互连接的网络节点

一个执行点,覆盖所有调用方,不需要客户端配合

2026-07-28 改了什么

现在一次 MCP 请求就是一个自包含的 HTTP POST,任何一个服务实例都能处理。没有握手、没有会话标识、没有客户端到实例的绑定。在线上它看起来就跟其它 JSON API 调用一样,于是 CDN 与 WAF 层的七层检查、变换与优化就可以用了,而且不用改 MCP 服务端代码。最后这一句最关键。需要改应用的安全控制,会被排到下个季度;摆在应用前面的控制,这周就能上线。

# What an MCP request looks like after the 2026-07-28 revision.
# No handshake, no session id, no sticky routing - one POST.
POST /mcp HTTP/1.1
Host: tools.example.com
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: query_database
Authorization: Bearer <token>
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query_database","arguments":{"filter":"id = 42"}}}

# Mcp-Method and Mcp-Name are mandatory on the Streamable HTTP
# transport. The server rejects any request where the headers and
# the body disagree, which is what makes the headers trustworthy
# signals for a firewall to route and block on.

值得押注的两个请求头

Streamable HTTP 传输上有两个强制头,它们存在的意义正是让负载均衡器、网关和防火墙不必解析请求体就能按操作做路由:Mcp-Method 携带操作(例如 tools/call),Mcp-Name 携带被调用的工具名。服务端会拒绝头与体不一致的任何请求。正是这个「拒绝」让这两个头成为可用于强制执行的可信信号,而不是一个你还得复核的提示。对 WAF 来说,一次 MCP 请求最关键的两个事实,在解析任何一个字节的 JSON 之前就已经可见了。

# Allowlist the MCP methods you actually expose. Anything else is
# blocked before it reaches the origin, so the origin never spends
# a token or a database connection on it.
import json

ALLOWED_METHODS = {'tools/list', 'tools/call', 'resources/read', 'prompts/list'}
ALLOWED_TOOLS   = {'query_database', 'search_docs', 'list_schemas'}

waf_rules = [
    {
        'Name': 'mcp-method-allowlist',
        'Priority': 1,
        'Action': {'Block': {}},
        'Statement': {'NotStatement': {'Statement': {'ByteMatchStatement': {
            'FieldToMatch': {'SingleHeader': {'Name': 'mcp-method'}},
            'PositionalConstraint': 'EXACTLY',
            'SearchString': m, 'TextTransformations': [{'Priority': 0, 'Type': 'LOWERCASE'}],
        }}}},
    } for m in sorted(ALLOWED_METHODS)
]

print(json.dumps(waf_rules, indent=2)[:400])
屏幕上的安全代码流

关键的两个头在解析请求体之前就可见了

新形态让九件事变简单

AWS 把它描述成新规范让「在边缘轻松执行」的九个用例,而规律是可以泛化的。给你的 MCP 方法列白名单,未批准的操作在源站消耗一个 token 之前就被拦掉;给工具名列白名单,这是你不读负载就能执行的、最接近真实能力边界的做法;按工具和调用方限流而不是按 IP,这样躲在同一个 NAT 网关后面的 Agent 不会共用一个桶;按调用方身份做过滤;观测并记录方法与工具的分布。这些都不需要厂商 SDK,也不需要改 MCP 服务端。

# Two headers give you the operation and the tool. That is enough to
# build a per-tool rate limit without parsing a single JSON body.
def tool_rate_limit(tool_name, limit_per_min=60):
    return {
        'Name': f'rl-{tool_name}',
        'Priority': 10,
        'Action': {'Block': {}},
        'Statement': {
            'RateBasedStatement': {
                'Limit': limit_per_min,
                'EvaluationWindowSec': 60,
                'AggregateKeyType': 'CUSTOM_KEYS',
                'CustomKeys': [
                    {'Header': {'Name': 'mcp-name', 'TextTransformations':
                                [{'Priority': 0, 'Type': 'LOWERCASE'}]}},
                    {'Header': {'Name': 'authorization', 'TextTransformations':
                                [{'Priority': 0, 'Type': 'NONE'}]}},
                ],
            }
        },
    }

# Rate limit per tool AND per caller, not just per IP. Agents behind
# one NAT gateway will otherwise share a single bucket.

如果头不够用,该检查哪里

头告诉你「操作」和「工具」,但不告诉你「参数」。当策略需要看具体值时,目标是唯一一个 JSON 路径:$.params.arguments——每一次 MCP 工具调用都把自己的参数放在那里;对数据库调用来说,就是它准备执行的那个 filter 字符串。负载里其余一切都是协议脚手架,把它当成有含义的东西,你就会写出「客户端库一升级就崩」的规则。应该窄范围检查、对明确已知的坏值做阻断,其余只记录,而不是基于「一周后你自己都解释不清的启发式」去阻断。

# When you need deeper inspection, the target is one JSON path.
# Every MCP tool call puts its per-tool arguments at $.params.arguments.
import json

def inspect_arguments(body: dict, banned_values):
    args = body.get('params', {}).get('arguments', {})
    hits = [v for v in args.values() if isinstance(v, str) and v in banned_values]
    return {'tool': body.get('params', {}).get('name'),
            'arg_keys': sorted(args),
            'blocked_values': hits,
            'decision': 'block' if hits else 'allow'}

# { "params": { "name": "query_database",
#               "arguments": { "filter": "id = 42" } } }
# Everything outside $.params.arguments is protocol scaffolding.
锁与加密概念图

边缘默认拒绝,源站只放行显式允许的方法

边缘执行做不到什么

WAF 无法判断一次被允许的工具调用「该不该调用」。一次说服 Agent 去调用它本来就有权限调用的工具的提示词注入,看起来跟正常请求一模一样——因为它本来就是。边缘规则缩小了攻击面、给了你可观测的地方,但它不对「意图」做授权判断。而且执行点的效果完全取决于部署是否正确:一个没有关联到分发的 web ACL,或者一条被前面终止性规则遮住的规则,实际上什么都拦不住,而且是静默失败——这比大声失败更糟。

# Prove the rule is attached and firing. An unenforced rule is a
# comment with a JSON accent.
#!/usr/bin/env bash
set -euo pipefail
ENDPOINT="https://tools.example.com/mcp"

code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$ENDPOINT" \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/call' \
  -H 'Mcp-Name: drop_everything' \
  -H 'Authorization: Bearer '"$TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"drop_everything","arguments":{}}}')

[ "$code" = "403" ] || { echo "expected 403 from WAF, got $code"; exit 1; }
echo 'WAF is enforcing the MCP tool allowlist'

把它真正落地

把 WAF web ACL 关联到 CloudFront 分发或 ALB,把白名单规则放在最前面,然后拿一个刻意不在白名单里的工具名去打端点,断言源站看到之前先收到 403。把这条检查放进 CI,因为没人测试的安全边界只是一个安全声明。收紧之前先看一周的阻断指标;第一天的阻断尖峰,通常意味着你漏登记了一个客户端,而不是有人在攻击。并且把执行点保持在那个「你完全可控、位于所有调用方之前、不管你用什么框架、走什么网络路径都要经过」的位置。

📌 常见问题 FAQ

2026-07-28 的 MCP 修订改了什么?

MCP 从有状态、绑定会话的协议改成了无状态的 HTTP 请求/响应设计。现在每次 MCP 请求都是自包含的 POST,任何服务实例都能处理,没有 initialize 握手,也没有用来固定路由的 Mcp-Session-Id。

边缘规则该用哪些请求头?

Mcp-Method 携带操作(例如 tools/call),Mcp-Name 携带工具名。两者在 Streamable HTTP 传输上都是强制的,且服务端会拒绝头与体不一致的请求,所以防火墙可以不解析请求体就基于它们做路由与阻断。

参数在负载的什么位置?

在 $.params.arguments。每一次 MCP 工具调用都把自己的参数放在那里。JSON 里其余都是协议脚手架,因此应只窄范围检查该路径,而不是写宽泛的请求体规则——后者在客户端库变更时会失效。

需要改我的 MCP 服务端吗?

不需要。AWS 的指引是让 WAF 与 CloudFront 在端点前面执行,就像你给任何 REST API 加一层那样,无需改动 MCP 服务端代码。你需要的是一个与分发或 ALB 关联的 web ACL,以及正确的规则顺序。

怎么确认规则真的生效了?

用一个不在白名单里的工具名发请求,断言在到达源站之前先收到来自 WAF 的 403。把这条检查放进 CI。未关联的 web ACL、或被更早的终止性规则遮住的规则,实际上什么都拦不住,而且会静默失败。