无状态 MCP Elicitation 多轮往返实战用 Cloudflare Agents 构建 Stateless MRTR 工具【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents导读本篇文章围绕examples/mcp-elicitation-mrtr这个官方示例完整讲解如何在 Cloudflare Workers 上以**无状态Stateless**方式实现 MCP 的 **Elicitation信息征求**能力通过increase-counter这个写入一次、全程无状态的工具演示一条 MCP 调用如何在多次独立请求multi-round-trip requests, MRTR中逐步收集输入、签名传递中间状态并最终返回结果。读完本文你将掌握inputRequired/inputResponse/acceptedContent的完整调用链、requestStateCodec签名状态的用法以及createMcpHandler无状态处理器的底层原理可以直接迁移到自己的 Worker 项目中。从有状态到无状态MRTR 解决什么问题传统的 MCP 服务器SDK v1 时代在处理需要向用户追问信息的场景时依赖会话状态服务端向客户端推送elicitation/create请求、用 Durable Object 保存会话初始化状态、并通过 SSE 重放消息。这套机制在需要挂起 Worker 请求、保存 pending Promise、跨请求共享服务器实例的前提下才能工作。而 Stateless Elicitation无状态征求换了一种思路不在服务端保留任何会话状态把中间数据打包成一份经过签名的requestState由客户端在下一轮请求中原样带回。每一轮 HTTP 请求到达 Worker 时都是一个全新的、干净的请求上下文工具函数根据当前轮客户端带回了什么来决定下一步。这正是mcp-elicitation-mrtrmulti-round-trip requests多轮往返请求的核心范式对应仓库中 handler-stateless.ts 所实现的createStatelessMcpHandler处理链路。increase-counter一次调用、两个输入轮次示例工具名为increase-counter语义上是让计数器增加 N。工具是 write-once写入一次且完全无状态一次callTool需要经历三轮请求第一轮客户端只带current当前值。服务器发现requestState为空返回input_required要求客户端提供amount增加量并把current封存进新签发的requestState。第二轮客户端带着amount与最新requestState重试。服务器从签名状态中读出第一步封存的current校验amount后再次返回input_required请求确认并把current amount封存进新的requestState。第三轮客户端带着confirm布尔确认与最新requestState重试。服务器验证签名、读取状态后返回普通的最终工具结果。每一轮重试只携带该轮所需的输入响应而签名后的requestState负责在一次次全新的 Worker 请求之间传递可信的中间数据。工具本身不挂起 Worker、不保存 pending Promise、也不在请求之间共享服务器实例。官方 README 中给出了requestState三阶段流转的精确定义本文后面会结合源码逐行拆解。项目结构一览examples/mcp-elicitation-mrtr是一个标准的 Wrangler Worker 项目结构如下examples/mcp-elicitation-mrtr/ ├── README.md # 官方说明文档 ├── src/ │ └── index.ts # 全部业务逻辑约 170 行 ├── env.d.ts # 生成的类型声明 ├── package.json # 依赖与脚本 ├── tsconfig.json └── wrangler.jsonc # Worker 配置依赖与脚本package.json 中的关键信息{ name: cloudflare/agents-mcp-elicitation-mrtr, type: module, scripts: { dev: wrangler dev, deploy: wrangler deploy, typecheck: tsc --noEmit, types: wrangler types env.d.ts --include-runtime false }, dependencies: { modelcontextprotocol/server: 2.0.0, agents: *, zod: ^4.5.4 }, devDependencies: { cloudflare/workers-types: ^5.20260729.1, typescript: ^6.0.3, wrangler: ^4.115.0 } }要点使用MCP SDK v2modelcontextprotocol/server2.0.0这是无状态 MRTR 的协议基础agents包提供createMcpHandler即无状态的createStatelessMcpHandler等 Worker 集成能力用zod做输入 schema 校验与解析。Wrangler 配置wrangler.jsonc{ $schema: ./node_modules/wrangler/config-schema.json, name: mcp-elicitation-mrtr-demo, main: src/index.ts, compatibility_date: 2026-06-11, compatibility_flags: [nodejs_compat], observability: { logs: { enabled: true } } }nodejs_compat兼容标志用于提供签名运算等 Node 兼容能力开启 observability 后生产环境可以直接在 Cloudflare 控制台查看 Worker 日志。本地运行与生产部署第一步准备签名密钥requestState的签发与验签依赖一个至少 32 字节的随机密钥。先在项目目录下写入本地开发密钥printf MRTR_REQUEST_STATE_KEYreplace-with-at-least-32-random-bytes\n .dev.vars pnpm install pnpm run devwrangler dev启动后MCP 端点位于http://localhost:8787/mcp。第二步连接无状态 MCP 客户端用任意支持 Stateless MCP 的客户端连接http://localhost:8787/mcp然后调用工具{ name: increase-counter, arguments: { current: 10 } }客户端会依次收到两轮input_required先要amount再要confirm完成全部输入后收到最终结果Counter increased by amount; next value is current amount。第三步生产部署部署前请使用wrangler secret put保存生产密钥不要复用本地示例值wrangler secret put MRTR_REQUEST_STATE_KEY pnpm run deploy从源码看入口文件 会强制校验密钥存在export default { fetch(request, env, ctx) { if (!env.MRTR_REQUEST_STATE_KEY) { throw new Error(MRTR_REQUEST_STATE_KEY must be configured); } // ... } }即未配置密钥时 Worker 直接拒绝启动请求避免在未签名状态上运行。核心实现深度解析状态类型与签名编解码器src/index.ts 首先定义了两个输入 schema 与一个有判别联合discriminated union状态const amountSchema z.object({ amount: z.number() }); const confirmationSchema z.object({ confirm: z.boolean() }); type CounterRequestState | { step: amount; current: number } | { step: confirmation; current: number; amount: number };CounterRequestState就是会被签名的中间数据第一步只封存current第二步封存current amount。它必须是一个可序列化、可校验的对象因为每次都会被打包进requestState交给客户端保管。requestStateCodec签名/验签的封装创建签名编解码器的代码位于 src/index.tsconst requestStateCodec createRequestStateCodecCounterRequestState({ key: env.MRTR_REQUEST_STATE_KEY, bind: ({ mcpReq }) mcpReq.method });其中key至少 32 字节的签名密钥来自环境变量bind把状态绑定到请求属性上示例中绑定的是mcpReq.method即每个状态只能被同名工具的请求消费增强防重放与防串用的能力。服务器初始化时把requestStateCodec.verify注入到McpServer配置中src/index.tsconst server new McpServer( { name: stateless-mrtr-elicitation-demo, version: 1.0.0 }, { requestState: { verify: requestStateCodec.verify } } );这样每个工具回调拿到的context.mcpReq.requestState都已经是通过验签的可信数据。第一轮请求 amount官方 README 中给出的关键模式Key pattern就是第一轮的核心const state context.mcpReq.requestStateCounterRequestState(); if (!state) { return inputRequired({ inputRequests: { amount: inputRequired.elicit({ message: By how much should the counter increase?, requestedSchema: { type: object, properties: { amount: { type: number } }, required: [amount] } }) }, requestState: await requestStateCodec.mint( { step: amount, current }, context ) }); }流程拆解context.mcpReq.requestStateCounterRequestState()取出本轮请求携带的已验证状态第一次调用时它为undefinedinputRequired({ ... })返回一个InputRequiredResult告诉客户端我还需要信息inputRequired.elicit({ message, requestedSchema })描述需要哪种输入requestedSchema是 JSON Schema对象 必填字段requestStateCodec.mint({ step: amount, current }, context)签名封存当前值作为下轮请求的requestState。注意requestState与inputRequests成对返回客户端必须把requestState原样带回否则下轮服务端无法验证。第二轮读取 amount请求确认当state.step amount时src/index.ts工具从本轮输入中提取 amountconst amountResponse inputResponse( context.mcpReq.inputResponses, amount ); if ( amountResponse.kind elicit amountResponse.action ! accept ) { return cancelled(); } const amount acceptedContent( context.mcpReq.inputResponses, amount, amountSchema ); if (!amount) return cancelled(); return inputRequired({ inputRequests: { confirmation: inputRequired.elicit({ message: Increase ${state.current} by ${amount.amount}?, requestedSchema: { type: object, properties: { confirm: { type: boolean } }, required: [confirm] } }) }, requestState: await requestStateCodec.mint( { step: confirmation, current: state.current, amount: amount.amount }, context ) });这里的关键 APIinputResponse(mcpReq.inputResponses, amount)读取该轮客户端提交的原始响应用于判断用户是accept还是取消/跳过acceptedContent(mcpReq.inputResponses, amount, amountSchema)用 zod schema 校验并提取真正被接受的内容失败返回undefined校验通过后用新的mint把{ step: confirmation, current, amount }签入下一轮requestState并请求confirm布尔确认。第三轮确认并返回最终结果state.step confirmation分支src/index.tsconst confirmationResponse inputResponse( context.mcpReq.inputResponses, confirmation ); if ( confirmationResponse.kind elicit confirmationResponse.action ! accept ) { return cancelled(); } const confirmation acceptedContent( context.mcpReq.inputResponses, confirmation, confirmationSchema ); if (!confirmation?.confirm) return cancelled(); const next state.current state.amount; return { content: [ { type: text, text: Counter increased by ${state.amount}; next value is ${next} } ] };第三轮不再返回inputRequired而是返回一个普通的CallToolResultcontent数组MCP 调用至此完成。state.current与state.amount全部来自上一轮签名封存的状态而不是客户端可随意篡改的明文参数——这就是签名状态携带可信中间数据的实际价值。取消场景统一走 cancelled()function cancelled(): CallToolResult { return { content: [{ type: text, text: Counter increase cancelled. }] }; }fetch 入口与 createMcpHandlersrc/index.ts 最终把所有请求交给createMcpHandlerreturn createMcpHandler(() createServer(requestStateCodec), { route: /mcp, legacy: reject })(request, env, ctx);两个选项很关键route: /mcp声明本 Worker 只处理/mcp路径其它路径返回 404legacy: reject明确拒绝走旧的SDK v1 / Durable Object 会话兼容路径强制所有请求走无状态 v2 处理链路。从源码看无状态处理器的底层保证createMcpHandler实际上就是createStatelessMcpHandler见 mcp/server/index.ts 的重导出。在 handler-stateless.ts 中可以看到几个值得注意的强制约束拒绝bus选项代码直接throw new TypeError因为 Agents SDK 不暴露有状态的事件总线拒绝一整套 legacy 选项transport、storage、sessionIdGenerator、eventStore、retryInterval等 SDK v1 选项一旦出现就会抛错提示managed SDK v2 handler 是无状态的请移除这些选项或在迁移期保留 v1 服务器handler-stateless.ts默认 host/origin 校验默认只接受 localhost 类主机、.workers.dev端点以及明确配置的 CORS Origin防御 DNS 重绑定攻击自定义域名部署时可显式传入allowedHostnames/allowedOriginHostnameshandler-stateless.ts。这些约束与示例中legacy: reject的配置相互印证无状态模型是当前 Agents 集成 MCP 的推荐路径而旧式状态化能力被刻意隔离。测试侧印证仓库测试 client-v2-mrtr.test.ts 用同款 API 组合验证了无状态 MRTR 行为statelessServer()注册的ask工具同样使用acceptedContent(context.mcpReq.inputResponses, ...)读取输入无输入时返回inputRequired({ inputRequests: { ...: inputRequired.elicit(...) } })并通过MCPClientConnection走完整往返。这证明示例中的写法就是官方无状态征求的标准模式可直接照搬到其他工具。与 Legacy Elicitation 的选择指南官方为两个示例明确划分了场景mcp-elicitation-mrtrStateless本示例适用于新部署。无会话、无 Durable Object、无 SSE 重放每次请求独立、天然可横向扩展适合绝大多数新的 MCP 服务器。mcp-elicitationLegacy仅当现有部署确实依赖以下能力时才保留——向客户端推送elicitation/create、Durable Object 会话状态、以及 SSE 重连重放。它显式使用createLegacyMcpHandler、DurableObjectEventStore、WorkerTransport等 SDK v1 API并提供了increase-counter表单模式与connect-accountURL 带外模式两个演示工具。选型判断如果只是向用户追问一个字段、确认一次操作优先选择本示例的无状态 MRTR 方案如果业务强依赖服务端主动推送、会话寻址或断线重放再考虑 Legacy 方案。小结examples/mcp-elicitation-mrtr用一个 170 行左右的 Worker 完整示范了 Stateless Elicitation 的工程形态requestStateCodec负责签名封存中间数据、inputRequired驱动多轮往返、acceptedContent校验每轮输入、createMcpHandler提供无状态入口。掌握这套模式后你可以为任意 MCP 工具叠加多步确认 / 动态补参 / 敏感操作复核等交互能力同时保持 Worker 的无状态可扩展特性。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考