diff --git a/internal/exec-plans/release-v1.153.0-plan.md b/internal/exec-plans/release-v1.153.0-plan.md index fcb0657e9..804626b61 100644 --- a/internal/exec-plans/release-v1.153.0-plan.md +++ b/internal/exec-plans/release-v1.153.0-plan.md @@ -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。 diff --git a/lime-rs/crates/mcp/Cargo.toml b/lime-rs/crates/mcp/Cargo.toml index 61e8ef563..e93eeffbb 100644 --- a/lime-rs/crates/mcp/Cargo.toml +++ b/lime-rs/crates/mcp/Cargo.toml @@ -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"] } diff --git a/lime-rs/crates/mcp/src/client_service.rs b/lime-rs/crates/mcp/src/client_service.rs index 25fed21a0..9926b0391 100644 --- a/lime-rs/crates/mcp/src/client_service.rs +++ b/lime-rs/crates/mcp/src/client_service.rs @@ -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 for LimeMcpClientService { ) -> Result<(), rmcp::ErrorData> { >::handle_notification( &self.handler, - notification, + normalize_notification(notification)?, context, ) .await @@ -136,6 +139,34 @@ impl Service for LimeMcpClientService { } } +fn normalize_notification( + notification: ServerNotification, +) -> Result { + 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 { @@ -158,3 +189,51 @@ struct CreateElicitationResultWithMeta { #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] meta: Option, } + +#[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::(), Some(&42)); + } +}