MCP 的下一代:无状态核心如何跑在 Workers 上

2026-08-06 55 预计阅读时间: 1 分钟
来源: blog.cloudflare.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

MCP 正在进入一个更适合生产环境的新阶段。下一代版本重写了核心,使其以无状态方式运行,并能够直接适配 Cloudflare Workers 这类边缘运行时。变化不只是协议字段的增删,还涉及功能生命周期、SDK 迁移路径,以及如何把原本依赖长连接和进程内状态的服务改造成可水平扩展的组件。

从有状态会话转向无状态核心

传统 MCP 服务往往容易把会话、初始化信息或工具上下文保存在进程内。这种方式在单机开发时简单,但部署到 Workers、Serverless 或多实例环境后会遇到几个问题:

  • 请求可能被路由到不同实例,进程内状态无法可靠复用。
  • 实例会被频繁创建和回收,长生命周期连接并不稳定。
  • 扩容时需要同步或迁移状态,增加了基础设施复杂度。
  • 测试和故障恢复更依赖隐藏的运行时上下文。

新的方向是让核心协议处理尽量保持无状态:每次请求都携带完成处理所需的信息,服务端只负责验证请求、执行工具并返回结果。需要持久化的数据则交给显式的数据存储,例如 KV、Durable Objects、数据库或外部 API。

这并不意味着所有 MCP 应用都不能保存状态,而是把“协议处理状态”和“业务状态”分开。协议层可以被重复创建、并发执行和快速回收;业务层状态则通过明确的存储接口管理。

Workers 运行时带来的工程约束

Workers 没有传统服务器那样稳定的常驻进程,也不适合依赖本地文件、全局变量或固定的 TCP 连接。因此,一个适合 Workers 的 MCP 实现通常需要满足以下条件:

  1. 处理函数可以在每个请求中独立完成工作。
  2. 不依赖本地磁盘保存会话或用户数据。
  3. 不假设请求总是到达同一个执行实例。
  4. 对外部服务调用设置超时、鉴权和错误映射。
  5. 将需要跨请求共享的数据交给显式存储。

可以把 MCP 入口设计成普通的 HTTP Worker。下面是一个最小示例,展示无状态请求处理的结构。这里的 parseMcpRequestrunToolformatMcpResponse 是示意函数,实际项目中应替换为目标 MCP SDK 提供的实现。

// src/worker.ts
interface Env {
  API_TOKEN: string;
}

type McpRequest = {
  method: string;
  params?: Record<string, unknown>;
};

function parseMcpRequest(input: unknown): McpRequest {
  if (!input || typeof input !== "object") {
    throw new Error("Invalid MCP request");
  }

  const value = input as Record<string, unknown>;
  if (typeof value.method !== "string") {
    throw new Error("Missing MCP method");
  }

  return {
    method: value.method,
    params: typeof value.params === "object" && value.params !== null
      ? value.params as Record<string, unknown>
      : undefined,
  };
}

async function runTool(
  name: string,
  args: Record<string, unknown> | undefined,
  env: Env,
): Promise<unknown> {
  if (name !== "get_status") {
    throw new Error(`Unknown tool: ${name}`);
  }

  const response = await fetch("https://api.example.com/status", {
    headers: { Authorization: `Bearer ${env.API_TOKEN}` },
  });

  if (!response.ok) {
    throw new Error(`Upstream status: ${response.status}`);
  }

  return response.json();
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    try {
      const mcpRequest = parseMcpRequest(await request.json());

      if (mcpRequest.method === "tools/call") {
        const name = String(mcpRequest.params?.name ?? "");
        const args = mcpRequest.params?.arguments as Record<string, unknown> | undefined;
        const result = await runTool(name, args, env);

        return Response.json({
          jsonrpc: "2.0",
          result: { content: [{ type: "text", text: JSON.stringify(result) }] },
        });
      }

      return Response.json({
        jsonrpc: "2.0",
        error: { code: -32601, message: `Unsupported method: ${mcpRequest.method}` },
      }, { status: 400 });
    } catch (error) {
      return Response.json({
        jsonrpc: "2.0",
        error: {
          code: -32603,
          message: error instanceof Error ? error.message : "Internal error",
        },
      }, { status: 500 });
    }
  },
};

这个示例有意没有使用全局 Map 保存会话,也没有依赖 Worker 实例固定不变。为了让它接近生产环境,还应补充请求鉴权、输入 Schema 校验、调用超时、日志关联 ID 和速率限制。

协议升级不只是改字段

下一代 MCP 的升级重点还包括协议能力的演进方式。协议长期运行时,新增功能必须考虑兼容性、实现顺序和回滚路径。一个实用的功能生命周期通常可以分成几个阶段:

  • 提案阶段:明确能力解决的问题、请求和响应形状,以及失败行为。
  • 实验阶段:允许 SDK 和早期用户试用,但标记为不稳定能力。
  • 稳定阶段:补齐互操作测试、文档和版本兼容策略。
  • 弃用阶段:保留迁移窗口,提供替代能力和清晰的移除时间点。

对使用者而言,最重要的不是记住每个阶段的名称,而是不要把实验性能力直接当成永久 API。服务端可以通过能力声明或版本协商判断客户端支持什么,客户端则应对未知字段和未知能力保持兼容。

例如,应用层可以把能力判断写成显式分支,而不是假设所有客户端都支持最新功能:

type ClientCapabilities = {
  supportsStructuredToolOutput?: boolean;
};

function buildToolResult(
  value: unknown,
  capabilities: ClientCapabilities,
) {
  if (capabilities.supportsStructuredToolOutput) {
    return {
      structuredContent: value,
      content: [{ type: "text", text: JSON.stringify(value) }],
    };
  }

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

这里的字段名称只是实践示例,具体字段应以目标版本的正式协议和 SDK 类型定义为准。核心思想是:能力探测和降级路径要进入代码,而不是只写在发布说明里。

SDK 迁移时先隔离协议边界

从旧 SDK 迁移到新 SDK 时,风险通常不在业务工具本身,而在初始化、传输层和错误处理方式发生变化。建议先把代码拆成三层:

  • 协议适配层:负责解析请求、声明能力、构造响应。
  • 工具注册层:负责工具名称、参数 Schema 和描述。
  • 业务执行层:负责访问数据库、调用外部 API 或执行实际任务。

这样迁移时可以只替换协议适配层和 SDK 初始化代码,业务执行层继续复用。迁移过程中应重点检查:

  • 初始化流程是否仍然需要服务端会话。
  • 传输层是否适合无状态部署。
  • 工具参数校验是否从运行时错误变成结构化错误。
  • 错误响应是否仍符合客户端预期。
  • 测试是否覆盖重复请求、并发请求和跨实例请求。

可以先用一个最小兼容测试验证迁移结果:

# 假设 Worker 已通过 wrangler 启动在本地 8787 端口
curl -sS http://127.0.0.1:8787 \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer local-test-token' \
  --data '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_status",
      "arguments": {}
    }
  }'

测试不应只验证 HTTP 状态码,还应检查响应是否包含预期的 JSON-RPC 结构、工具结果和可识别的错误信息。对于需要持久状态的业务,则应单独测试存储读写和并发冲突。

生产采用清单

下一代 MCP 更适合边缘运行时,但“无状态”不会自动解决所有部署问题。上线前可以按下面的清单检查:

  • 协议请求是否可以在不同实例之间独立处理?
  • 是否误用了全局变量、本地文件或进程内会话缓存?
  • 业务状态是否明确放在 KV、数据库或其他持久化系统中?
  • 所有外部调用是否都有超时和失败映射?
  • 客户端不支持新能力时,服务端是否有降级行为?
  • SDK 升级是否覆盖初始化、传输、工具注册和错误处理?
  • 是否验证了重复请求、并发请求、冷启动和跨区域调用?

对于简单的只读工具,无状态迁移通常可以从一个小服务开始;对于需要长任务、实时事件或强一致会话的场景,则需要进一步评估 Durable Objects、队列或专用后端。下一代 MCP 的价值在于降低协议核心对运行时的假设,让开发者可以按业务需要选择部署平台,而不是让部署平台反过来决定协议实现方式。


相关推荐