通过 MCP Tasks 暴露持久工作
把 Workflow Task 映射为 MCP 句柄,支持轮询、更新、取消、订阅、保留和租户安全授权。
Task 协议
普通 MCP Tool Call 在一次请求内完成;MCP Task 表示比请求寿命更长的工作。Client 创建
Task 后得到不透明 taskId、当前状态、轮询建议、Retention 时间,以及最终结果或安全失败
细节。
Task 需要协商 Capability 与逐请求授权。不支持该扩展的 Server 继续使用普通 MCP 行为。
启用 Task 扩展
Workflow Adapter 是 runifold-mcp 的 Opt-in Feature:
[dependencies]
runifold = { version = "=0.9.0", features = ["mcp", "sqlite-bundled"] }
runifold-mcp = { version = "=0.9.0", features = ["workflow-tasks"] }Client 在初始化时声明 Task 支持:
let config = McpClientConfig::new(Implementation::new("task-client", "1"))
.with_tasks();
let client = McpClient::new(transport, config);
let initialized = client.initialize().await?;
println!("task capability: {:?}", initialized.capabilities.tasks);如果 Peer 没有协商 Task,就使用普通 Request / Response 调用 Tool,或禁用长任务模式。 不能假设所有 Tool Call 都可以变成 Task。
工作流映射
启用 workflow-tasks Feature 后,WorkflowTaskAdapter 把一个 MCP Tool Route 绑定到
精确 Workflow Name、Version 与 Tenant。Queued、Leased、Timer 与 Signal Wait 映射为
working;持久 Interrupt 映射为 input_required;终止 Checkpoint 映射为 Completed、
Failed 或 Cancelled。
结果从不可变 Terminal History 重建。Route Identity 必须稳定,使 Task ID 在 Server 重启后仍能恢复相同契约。
客户端生命周期
Client 可以显式:
- 用
get_task读取当前状态; - 用
update_task提交带 Key 输入; - 用
cancel_task协作取消; - 用
wait_task在同一个操作 Deadline 下等待; - 用
listen_tasks接收类型化 Snapshot。
Polling Interval 受 Client Policy、剩余 Deadline 与 Retention 共同约束。isError: true
的已完成 Tool Result 仍是 Completed Task;协议失败属于另一层。
调用 Tool 并处理两种 Outcome
use runifold::mcp::{CallToolOutcome, CallToolParams};
let outcome = client
.call_tool_outcome(CallToolParams {
name: "durable_report".into(),
arguments: Some(serde_json::Map::from_iter([
("report_id".into(), serde_json::json!("rpt_42")),
])),
})
.await?;
let result = match outcome {
CallToolOutcome::Complete(result) => result,
CallToolOutcome::Task(task) => {
println!("task={} status={:?}", task.task_id, task.status);
client.wait_task(task).await?
}
};
if result.is_error {
eprintln!("Tool completed with an application error: {:?}", result.content);
} else {
println!("structured={:?}", result.structured_content);
}wait_task 按 Server 建议 Poll,同时遵守 Client Operation Deadline。如果 UI 或进程可能
断开,应把 Task ID 保存在应用状态中;重连后先 get_task,再调用 wait_task。持久
Store 才是真实来源,内存 Subscription 不是。
订阅
Task Subscription 立即发送持久 Snapshot,之后只发送变化状态,并在 Terminal Snapshot 后解除。重连读取当前 Store 状态,而不依赖进程内 Notification Replay。
限制每个 Subscription 的 Task ID 数与刷新频率。临时 Store 失败不能虚构状态跳转。每次 派生状态时都重新验证授权与租户隔离。
安全与保留
Task ID 是标识符,不是 Bearer Credential。每次 Lookup 都限定在 Adapter Tenant;跨租户 不匹配不应泄漏存在性。Task Input 与普通 MCP Elicitation 或 Sampling 具有相同信任等级。
协议 ttlMs 描述从创建开始的句柄可用性,不是 Workflow Deadline,也不会物理删除活跃
工作。Terminal Cleanup 属于另一个带栅栏控制面,并生成不可变 Tombstone。
把 Tool Route 绑定到 Workflow
使用 Worker 相同的持久 WorkflowStore 构造 WorkflowTaskAdapter,并注册稳定映射:
let mut tasks = WorkflowTaskAdapter::new(store.clone());
tasks.register_route(WorkflowTaskRoute::new(
"durable_report", // MCP Tool 名称
"generate-report", // Workflow 名称
1, // 准确 Workflow 版本
WorkflowTenantId::parse("tenant-acme")?,
)?)?;
let server = server.with_task_backend(Arc::new(tasks));MCP Server 还必须注册并授权这个 Tool。独立 Workflow Worker 必须注册
generate-report 版本 1,并处理同一个 Store。MCP Transport 重启不会取消或删除 Task。
Task 故障排查
| 现象 | 检查项 |
|---|---|
| 调用总是同步完成 | Task Capability、Route Registration 与 Tool Name 是否一致 |
DefinitionUnavailable | Worker Registry 是否包含准确 Workflow 名称/版本 |
Task 一直 working | Queue Claim、Tenant Budget、Timer、Lease 与 Worker 健康 |
Task 为 input_required | 展示带 Key Request,并用 update_task 对每个 Key 只提交一次 |
| 重连后进度丢失 | 是否使用同一 Durable Store 与已保存 Task ID |
| 跨租户查询泄露存在性 | Adapter Route 与每次 Lookup 是否绑定认证 Tenant |
Completed Result 带 is_error | 按应用完成处理,不要当成协议失败 |