fix(mcp): dispatch progress with arbitrary-precision JSON

This commit is contained in:
coso
2026-10-09 22:47:17 +08:00
parent 8e45bf7730
commit 8e50db2f73
3 changed files with 95 additions and 2 deletions
@@ -119,3 +119,15 @@
- Docs最终生成exit0且无预渲染错误,60条route完成;13/13 sitemap、canonical与og:url一致(主页仅标准化尾斜杠),14/14 llms公开链接均有实际文件,`llms-full.txt` 34661 bytes;无重复base path。证据为 `docs-ci/docs-mdast-fixed-generate.log`、`docs-ci/docs-validation-summary.json`。独立锁文件保留解析后的Content3.15.2/MDC0.22.2,不再额外覆盖其版本;只有Markdown序列化依赖的2.1.2 override。Docs边界检查通过、既有守卫24/24通过、静态验证7/7通过。
- 修复提交写集仅为Docs四文件(含独立生成锁文件)、Pages workflow、MCP现有测试和本计划,共7文件;用户全局gitignore忽略pnpm-lock.yaml,显式纳入这一个必要锁文件,不修改全局规则。其余CLI/TUI/架构/运维文档开发均保留。使用独立索引复核及正常hook提交,不重打tag。
- 当前Quality run `37932718117` 最终cancelled:MCP并发进度测试从13:13:36至13:51:24持续等待,触发60分钟job预算;其余任务成功。新增认领 `lime-rs/crates/mcp/src/bridge_client.rs` 的现有测试区,先复现、定位无限等待,再定向验证,不以提高作业timeout掩盖卡死。
- CI修复提交 `8e45bf77308ce50d719340a0030f3809a491178a` 已推送main,唯一父为本版release commit;远端v1.153.0仍保持原SHA。正常pre-commit实际执行6个匹配路径全部通过(workflow另有显式静态验证),未跳过hook;独立索引仅同步本轮7路径,所有并行开发保留。
- Docs恢复run `37942348705` 的build/deploy均success:https://github.com/limecloud/lime/actions/runs/37942348705 。公开Pages实读首页、sitemap、llms索引/完整文本、API正文和raw Markdown共6个HTTP200;13条sitemap和canonical正确,完整文本34661 bytes,与本地生成一致(`docs-ci/docs-public-summary.json`)。Docs失败已闭环;新Quality run `37942348723` 继续核验MCP等待修正。
- Quality恢复run `37942348723` 的Rust Full在MCP169项中168通过/1失败,5秒诊断明确为工具response成功但token0没有progress;首次test-only等待修正只揭露问题,未修复根因。Frontend Full、Bridge/Contracts、Integrity、GUI Smoke均成功,Windows继续运行。
- 依赖图对照及最小wire复现确认:`execpolicy -> starlark`在完整workspace/CLI图启用`serde_json/arbitrary_precision`,RMCP0.12的untagged通知解析把含浮点`progress/total`的标准`notifications/progress`落到CustomNotification,因此没有调用进度dispatcher。本地只开preserve_order仍通过;开启arbitrary_precision的真实duplex回归继续验证失败。最小复现`mcp-wire-repro.log`同时证明直接解析ProgressNotificationParam成功、同一wire解析ServerNotification落到CustomNotification。
- 本轮扩展窄写集至`lime-rs/crates/mcp/src/client_service.rs`和`lime-rs/crates/mcp/Cargo.toml`:仅在唯一current客户端服务边界按标准method恢复强类型进度通知,再交回既有handler;test依赖启用生产图已有arbitrary_precision以稳定覆盖真实transport回归。未知自定义method继续既有路径,非法标准参数返回协议错误;不创建第二dispatcher或runtime、不提高CI预算。
- 公开npm五包与provenance/四平台包结构全部核验;真实registry安装后13个文件与已验证tarball SHA256匹配、launcher symlink正确,`lime --version`=1.153.0。已发布CLI完整stdio及human双TTY成功(`npm-public-flow.log`,thread `01a12114-4406-7830-a831-42064abf251a`、turn `turn_0dc86d5284de45bebe09ca43df96722c`)。已发布TUI complete首轮180秒总预算未完成,保留timeout并定向续验,不计为通过。
- 开启arbitrary_precision的现有真实duplex用例在修复前稳定重现`call A: no progress for token Number(0)`(`mcp-arbitrary-precision-repro.log` exit101),与CI完全相同;边界修复后MCP库172/172通过(`mcp-normalized-tests.log` exit0)。dev依赖只增加已有serde_json特性,不变版本/锁文件;生产统一图与定向测试从此一致。新增回归覆盖浮点wire、非法标准参数拒绝、未知method及extensions保留。
- `npm run test:contracts`完整exit0(`contracts-mcp-normalized.log`),`npm run verify:app-version`通过=1.153.0;本轮没有新App Server/IPC方法或schema变化。现有progress隔离断言及有界诊断全部保留。
- 公开npm TUI续验已完成complete PTY、输入/导航/补全/推理、stdio reasoning/cold resume、focus/reconnect;最后resize四项中一项未观察到启动输入,原日志保留(`npm-tui-public-retry.log`)。原四项未改代码/超时定向重验4/4通过(`npm-tui-resize-retry.log` exit0),完整入口再次收口以保留最终ledger/event序列断言。
- 本次提交仅MCP client_service、MCP dev-dependency manifest与发布计划三文件;之前的bridge_client测试修改已在main。current为唯一MCP服务的标准通知解析,test-only为生产feature图回归,compat/deprecated/dead无新增。用户授权继续完整发布,窄提交/推送沿用既有授权;v1.153.0公开tag保持`3528a3c7`,生产修复进入main,不暗改已发布binary。
+2
View File
@@ -31,3 +31,5 @@ libc = "0.2"
[dev-dependencies]
futures.workspace = true
tempfile.workspace = true
# Match the CLI/workspace feature graph when testing the real MCP wire.
serde_json = { workspace = true, features = ["arbitrary_precision"] }
+81 -2
View File
@@ -7,7 +7,10 @@
use crate::client::LimeMcpClient;
use crate::elicitation::ElicitationResponse;
use lime_core::DynEmitter;
use rmcp::model::{ClientInfo, ClientResult, CustomResult, ServerNotification, ServerRequest};
use rmcp::model::{
ClientInfo, ClientResult, ConstString, CustomResult, ProgressNotification,
ProgressNotificationMethod, ServerNotification, ServerRequest,
};
use rmcp::service::{NotificationContext, RequestContext, Service};
use rmcp::RoleClient;
use serde::Serialize;
@@ -125,7 +128,7 @@ impl Service<RoleClient> for LimeMcpClientService {
) -> Result<(), rmcp::ErrorData> {
<LimeMcpClient as Service<RoleClient>>::handle_notification(
&self.handler,
notification,
normalize_notification(notification)?,
context,
)
.await
@@ -136,6 +139,34 @@ impl Service<RoleClient> for LimeMcpClientService {
}
}
fn normalize_notification(
notification: ServerNotification,
) -> Result<ServerNotification, rmcp::ErrorData> {
match notification {
ServerNotification::CustomNotification(notification)
if notification.method == ProgressNotificationMethod::VALUE =>
{
// RMCP's untagged enum can classify floating-point progress as custom
// when the CLI's dependency graph enables serde_json/arbitrary_precision.
let params = serde_json::from_value(notification.params.unwrap_or(Value::Null))
.map_err(|error| {
rmcp::ErrorData::invalid_params(
format!("invalid progress notification: {error}"),
None,
)
})?;
Ok(ServerNotification::ProgressNotification(
ProgressNotification {
method: ProgressNotificationMethod,
params,
extensions: notification.extensions,
},
))
}
notification => Ok(notification),
}
}
fn elicitation_response_result(
response: ElicitationResponse,
) -> Result<CustomResult, rmcp::ErrorData> {
@@ -158,3 +189,51 @@ struct CreateElicitationResultWithMeta {
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
meta: Option<Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::{CustomNotification, NumberOrString, ProgressToken};
use serde_json::json;
#[test]
fn fractional_progress_wire_reaches_typed_handler() {
let wire = r#"{"method":"notifications/progress","params":{"progressToken":7,"progress":0.25,"total":1.5,"message":"working"}}"#;
let notification: ServerNotification = serde_json::from_str(wire).unwrap();
let notification = normalize_notification(notification).unwrap();
let ServerNotification::ProgressNotification(notification) = notification else {
panic!("standard progress must reach the typed handler");
};
assert_eq!(
notification.params.progress_token,
ProgressToken(NumberOrString::Number(7))
);
assert_eq!(notification.params.progress, 0.25);
assert_eq!(notification.params.total, Some(1.5));
assert_eq!(notification.params.message.as_deref(), Some("working"));
}
#[test]
fn invalid_standard_progress_returns_protocol_error() {
for params in [None, Some(json!({"progressToken": 7, "progress": "bad"}))] {
let notification = CustomNotification::new(ProgressNotificationMethod::VALUE, params);
let error = normalize_notification(notification.into()).unwrap_err();
assert_eq!(error.code, rmcp::model::ErrorCode::INVALID_PARAMS);
}
}
#[test]
fn custom_notification_keeps_payload_and_extensions() {
let params = json!({"progress": 0.25, "domain": "custom"});
let mut notification =
CustomNotification::new("notifications/custom", Some(params.clone()));
notification.extensions.insert(42_u32);
let notification = normalize_notification(notification.into()).unwrap();
let ServerNotification::CustomNotification(notification) = notification else {
panic!("unknown methods must remain custom notifications");
};
assert_eq!(notification.method, "notifications/custom");
assert_eq!(notification.params, Some(params));
assert_eq!(notification.extensions.get::<u32>(), Some(&42));
}
}