欢迎光临
我们一直在努力

【随笔】MCP请求取消:停止等待以后,远端结果仍要核对

上一篇讨论MCP进度通知,进度达到100%后仍要等待最终结果。今天继续看Agent界面里的“停止”按钮:用户按下以后,客户端怎样表达取消,服务端怎样收尾,迟到结果又应该如何处理?

取消涉及协议版本、传输方式和应用状态三个层面。本文依据当日核对的官方规范与TypeScript SDK v2文档,区分2025时代的通知机制和2026-07-28的Streamable HTTP行为。代码是本地Python状态模型,不连接真实服务,也不宣称完成SDK联调。

一、结构介绍:取消意图、执行收尾与业务结果

用户停止等待,不代表远端已经停止执行;远端停止执行,也不代表之前提交的业务动作已经撤回。界面应清楚展示自己实际知道的状态。

层面可以确认的内容不能据此推出的结论
用户点击停止 本地不再希望继续等待 远端已经停止
客户端发出取消信号 取消意图已交给传输机制 已撤回外部写入
服务端处理取消 按实现停止工作并释放资源 所有副作用自动回滚
应用查询业务状态 读取持久化结果 单靠界面状态即可推断

MCP取消意图与业务结果核对流程

图中“取消意图”和“业务状态核对”是两个不同的信息来源。发送、收尾与核对分别处理,长任务才容易解释。

二、先分清版本与传输方式

当日核对的TypeScript SDK v2版本迁移说明明确区分:2026-07-28的Streamable HTTP连接通过关闭该请求的SSE响应流表达取消;2025时代连接以及各版本的stdio仍使用notifications/cancelled。因此不能对所有MCP连接套用同一条发送逻辑。

现代HTTP请求流。客户端abort对应请求时,取消的是该请求的响应流。不要把它画成删除整个会话或主动关闭所有请求的通道。

通知机制。下面只展示通知形式,适用于采用这一机制的连接;不能直接当作2026-07-28 HTTP的取消示例:

{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {
"requestId": 7,
"reason": "用户停止等待"
}
}

通知没有独立的JSON-RPC请求id,也没有对应的确认响应;params中的requestId用于指向此前发出的活动请求。通知方向与被取消请求的发出方向一致。2025-06-18取消规范还规定客户端不能取消initialize握手,且需要处理未知请求和已完成请求等情况。

三、停止等待以后,怎样处理迟到响应

请求完成与取消可能交叉发生:服务端已经提交结果,取消信号才到达;或者客户端已经决定停止等待,结果随后到达。

客户端可采用一条明确的本地策略:记录CANCEL_REQUESTED,结束本地等待后进入STOPPED_LOCALLY,迟到响应不再覆盖这个显示状态。这些名称由应用定义,不是MCP新增的状态字段。

双猫演示本地停止与远端待核对

图中的界面已经停止等待,但远端工作卡仍保留“待核对”。应用不能把本地清理成功写成服务端已撤回任务。对文件生成、消息发送或数据库写入,仍需业务查询或补偿机制。

四、一个可运行的本地状态模型

示例只演示信号选择和迟到响应策略。modern-http代表2026-07-28 HTTP路径;notification代表通知路径。返回的action只是文字标记,没有实际发送网络消息。

保存为cancel_demo.py:

"""Local application-state model: no SDK, server or network is used."""
from dataclasses import dataclass

@dataclass
class Pending:
request_id: int
mode: str
state: str = "RUNNING"

class Tracker:
def __init__(self):
self.requests = {}

def begin(self, request_id, mode):
if request_id in self.requests:
raise ValueError("request id already used")
if mode not in ("modern-http", "notification"):
raise ValueError("unknown mode")
self.requests[request_id] = Pending(request_id, mode)

def cancel(self, request_id):
item = self.requests.get(request_id)
if item is None or item.state != "RUNNING":
return "IGNORE_NOT_RUNNING"
item.state = "CANCEL_REQUESTED"
if item.mode == "modern-http":
return "CANCEL_REQUESTED action=CLOSE_REQUEST_SSE"
return "CANCEL_REQUESTED action=SEND_CANCEL_NOTIFICATION"

def response(self, request_id):
item = self.requests.get(request_id)
if item is None:
return "IGNORE_UNKNOWN_RESPONSE"
if item.state != "RUNNING":
return "IGNORE_LATE_RESPONSE"
item.state = "SUCCEEDED"
return "SUCCEEDED"

def cleanup_local(self, request_id):
item = self.requests[request_id]
if item.state == "CANCEL_REQUESTED":
item.state = "STOPPED_LOCALLY"
return item.state

if __name__ == "__main__":
tracker = Tracker()
tracker.begin(7, "modern-http")
print(tracker.cancel(7))
print(tracker.response(7))
print(tracker.cleanup_local(7))
tracker.begin(8, "notification")
print(tracker.cancel(8))
print(tracker.cleanup_local(8))
tracker.begin(9, "modern-http")
print(tracker.response(9))
print(tracker.cancel(9))

运行:

python cancel_demo.py

本次使用Python3.12.7的实际输出:

CANCEL_REQUESTED action=CLOSE_REQUEST_SSE
IGNORE_LATE_RESPONSE
STOPPED_LOCALLY
CANCEL_REQUESTED action=SEND_CANCEL_NOTIFICATION
STOPPED_LOCALLY
SUCCEEDED
IGNORE_NOT_RUNNING

请求7选择关闭请求SSE的动作,迟到响应被忽略;请求8选择取消通知。两者的本地终态均为STOPPED_LOCALLY,这个结果只证明模型停止等待。请求9先收到成功结果,随后取消被忽略,防止已完成状态被随意改写。

示例保留已使用的请求id以阻止复用,实际应用可按连接划分记录,并根据容量策略清理。它没有实现连接故障、持久任务查询或服务端取消处理器。

五、服务端收尾与接入注意事项

协作式停止。取消到达后,业务代码应在合适的检查点响应,释放文件句柄、临时资源和后台工作。无法立即中断的外部调用,需要按自身合同处理,不能只改界面文案。

响应行为要按版本实现。Python SDK v2迁移指南解释了取消请求后的响应收尾,以及2025时代HTTP兼容路径的例外。接入时使用对应SDK的取消机制,避免自行补发一条“取消成功”的普通结果来混淆原请求。

Tasks另有取消入口。普通请求取消与持久任务取消不能互换。Tasks扩展规范使用tasks/cancel,明确禁止以notifications/cancelled取消task;收到取消确认后,任务状态也可能暂未变成cancelled。本文模型只处理普通请求。

重试前先核对副作用。客户端停止等待后,远端动作可能已提交。再次调用前,结合业务查询和幂等键处理,避免把取消当作“肯定没有执行”。

版本不从包名猜测。TypeScript SDK v2不会仅因升级包就自动让所有连接采用2026时代行为。以实际协商结果、传输方式和对应配置为准;版本差异见官方协议版本说明。

六、🧠 思维导图

#mermaid-svg-1fgMoh25Dpsol7nz{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1fgMoh25Dpsol7nz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1fgMoh25Dpsol7nz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1fgMoh25Dpsol7nz .error-icon{fill:#552222;}#mermaid-svg-1fgMoh25Dpsol7nz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1fgMoh25Dpsol7nz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1fgMoh25Dpsol7nz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1fgMoh25Dpsol7nz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1fgMoh25Dpsol7nz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1fgMoh25Dpsol7nz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1fgMoh25Dpsol7nz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1fgMoh25Dpsol7nz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1fgMoh25Dpsol7nz .marker.cross{stroke:#333333;}#mermaid-svg-1fgMoh25Dpsol7nz svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1fgMoh25Dpsol7nz p{margin:0;}#mermaid-svg-1fgMoh25Dpsol7nz .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1fgMoh25Dpsol7nz .cluster-label text{fill:#333;}#mermaid-svg-1fgMoh25Dpsol7nz .cluster-label span{color:#333;}#mermaid-svg-1fgMoh25Dpsol7nz .cluster-label span p{background-color:transparent;}#mermaid-svg-1fgMoh25Dpsol7nz .label text,#mermaid-svg-1fgMoh25Dpsol7nz span{fill:#333;color:#333;}#mermaid-svg-1fgMoh25Dpsol7nz .node rect,#mermaid-svg-1fgMoh25Dpsol7nz .node circle,#mermaid-svg-1fgMoh25Dpsol7nz .node ellipse,#mermaid-svg-1fgMoh25Dpsol7nz .node polygon,#mermaid-svg-1fgMoh25Dpsol7nz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1fgMoh25Dpsol7nz .rough-node .label text,#mermaid-svg-1fgMoh25Dpsol7nz .node .label text,#mermaid-svg-1fgMoh25Dpsol7nz .image-shape .label,#mermaid-svg-1fgMoh25Dpsol7nz .icon-shape .label{text-anchor:middle;}#mermaid-svg-1fgMoh25Dpsol7nz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1fgMoh25Dpsol7nz .rough-node .label,#mermaid-svg-1fgMoh25Dpsol7nz .node .label,#mermaid-svg-1fgMoh25Dpsol7nz .image-shape .label,#mermaid-svg-1fgMoh25Dpsol7nz .icon-shape .label{text-align:center;}#mermaid-svg-1fgMoh25Dpsol7nz .node.clickable{cursor:pointer;}#mermaid-svg-1fgMoh25Dpsol7nz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1fgMoh25Dpsol7nz .arrowheadPath{fill:#333333;}#mermaid-svg-1fgMoh25Dpsol7nz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1fgMoh25Dpsol7nz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1fgMoh25Dpsol7nz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1fgMoh25Dpsol7nz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1fgMoh25Dpsol7nz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1fgMoh25Dpsol7nz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1fgMoh25Dpsol7nz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1fgMoh25Dpsol7nz .cluster text{fill:#333;}#mermaid-svg-1fgMoh25Dpsol7nz .cluster span{color:#333;}#mermaid-svg-1fgMoh25Dpsol7nz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1fgMoh25Dpsol7nz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1fgMoh25Dpsol7nz rect.text{fill:none;stroke-width:0;}#mermaid-svg-1fgMoh25Dpsol7nz .icon-shape,#mermaid-svg-1fgMoh25Dpsol7nz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1fgMoh25Dpsol7nz .icon-shape p,#mermaid-svg-1fgMoh25Dpsol7nz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1fgMoh25Dpsol7nz .icon-shape .label rect,#mermaid-svg-1fgMoh25Dpsol7nz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1fgMoh25Dpsol7nz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1fgMoh25Dpsol7nz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1fgMoh25Dpsol7nz :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

MCP请求取消

信号选择

2026 HTTP关闭请求SSE

旧版连接与stdio使用通知

本地状态

记录停止意图

处理迟到响应与清理

远端收尾

协作停止并释放资源

业务副作用单独核对

边界

Tasks使用专门取消入口

重试先查状态与幂等合同

七、总结

总结要点

取消信号随连接而变。先核对协议版本与传输方式,再选择通知或关闭请求响应流。普通请求和持久任务也有不同入口。

本地停止与远端结果分别记录。迟到响应不应随意覆盖用户已经结束的界面状态;业务是否完成,需要可靠的状态来源。

收尾需要业务配合。资源释放、外部副作用和重试幂等应在服务实现中明确。停止等待本身不能证明已完成回滚。

下一篇继续看MCP请求级日志:Agent怎样选择日志级别,并把诊断信息与工具结果分开处理。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

赞(0)
未经允许不得转载:171主机测评 » 【随笔】MCP请求取消:停止等待以后,远端结果仍要核对
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址