From e8aad0b7393187e63cb2ab91ab1b19c7273d5bb5 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 3 Aug 2026 21:32:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DMCP=E6=97=A7=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E9=94=99=E8=AF=AF=E6=96=87=E6=9C=AC=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 legacy MCP 结构与目录绑定错误收敛为稳定类别 避免 serde 原始错误回显模型提交的私密字符串 补充结构错误和未知目录绑定的不泄密测试与踩坑说明 --- .../src-tauri/src/mcp.rs | 53 +++++++++++++++++-- docs/project-memory/shared-memory/pitfalls.md | 2 +- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/mcp.rs index a7b216e2a..b4d79f7f9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/mcp.rs @@ -1257,12 +1257,12 @@ pub(crate) fn enrich_game_creator_mcp_actions( continue; } let input = serde_json::from_value::(action.input.clone()) - .map_err(|error| format!("MCP action input 无效:{error}"))?; + .map_err(|_| "MCP action input 结构无效".to_string())?; let tool = catalog .tools .iter() .find(|tool| tool.server_id == input.server && tool.name == input.tool) - .ok_or_else(|| format!("MCP catalog 不包含 {}/{}", input.server, input.tool))?; + .ok_or_else(|| "MCP action 未绑定当前 catalog tool".to_string())?; validate_game_creator_mcp_tool_arguments( tool, &serde_json::Value::Object(input.arguments.clone()), @@ -1274,7 +1274,7 @@ pub(crate) fn enrich_game_creator_mcp_actions( catalog_fingerprint: catalog.fingerprint.clone(), tool_fingerprint: tool.fingerprint.clone(), }) - .map_err(|error| format!("构造 MCP action identity 失败:{error}"))?; + .map_err(|_| "构造 MCP action identity 失败".to_string())?; } Ok(()) } @@ -1312,7 +1312,7 @@ pub(crate) fn validate_game_creator_mcp_tool_arguments( pub(crate) fn parse_game_creator_mcp_call_input( value: &serde_json::Value, ) -> Result { - serde_json::from_value(value.clone()).map_err(|error| format!("MCP call input 无效:{error}")) + serde_json::from_value(value.clone()).map_err(|_| "MCP call input 结构无效".to_string()) } pub(crate) fn game_creator_mcp_tool_effective_approval( @@ -2253,6 +2253,51 @@ mod tests { .expect("schema-valid MCP arguments should be enriched"); } + #[test] + fn mcp_enrichment_errors_do_not_echo_legacy_private_values() { + let catalog = mcp_catalog(""); + let parse_marker = "MCP_DURABLE_INPUT_PRIVATE_MARKER"; + let parse_error = parse_game_creator_mcp_call_input(&serde_json::json!(parse_marker)) + .expect_err("invalid durable MCP input must fail closed"); + assert!(!parse_error.contains(parse_marker)); + + for (label, input, private_marker) in [ + ( + "invalid-shape", + serde_json::json!("MCP_LEGACY_INPUT_PRIVATE_MARKER"), + "MCP_LEGACY_INPUT_PRIVATE_MARKER", + ), + ( + "unknown-catalog-binding", + serde_json::json!({ + "server": "MCP_LEGACY_SERVER_PRIVATE_MARKER", + "tool": "MCP_LEGACY_TOOL_PRIVATE_MARKER", + "arguments": {} + }), + "MCP_LEGACY_SERVER_PRIVATE_MARKER", + ), + ] { + let mut plan = AgentRuntimeToolPlan { + thinking_summary: label.to_string(), + plan_update: None, + plan: Vec::new(), + actions: vec![AgentRuntimeToolAction { + tool: GAME_CREATOR_MCP_CALL_TOOL.to_string(), + reason: None, + input, + }], + response: String::new(), + }; + let error = enrich_game_creator_mcp_actions(&mut plan, &catalog) + .expect_err("invalid legacy MCP action must fail closed"); + assert!(!error.contains(private_marker), "{label}: {error}"); + assert!( + !error.contains("MCP_LEGACY_TOOL_PRIVATE_MARKER"), + "{label}: {error}" + ); + } + } + #[test] fn mcp_argument_validation_accepts_local_refs_and_rejects_external_refs() { let local_schema = serde_json::json!({ diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 2aa79342c..a8d7d5e31 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4094,5 +4094,5 @@ - 现象:动态 MCP 函数虽然带 catalog `inputSchema`,Runtime 却只检查 `arguments.input` 是 object;非 strict 或兼容 Provider 可以返回缺 required、类型错误、enum 外值或 schema 外隐藏字段,并把它们原样送到外部工具。 - 风险:Provider 工具约束不是本地安全边界;特别是 `writes + readOnlyHint=true` 自动放行的工具,schema 外字段可能改变外部副作用而不进入预期确认路径。 -- 处理:使用完整 JSON Schema validator 校验原始 catalog schema,不手写 required/type 子集;native parser、fingerprint enrichment 与实际 MCP 调用边界复用同一校验器。enrichment 错误必须映射回 classified `arguments-schema` repair,不能以普通字符串直接终止 run;执行点重验用于阻断升级前已经落盘的 schema 外 pending。关闭网络和文件 `$ref` 解析,schema 无法安全编译时不广告或不执行,错误只返回稳定类别,不拼接 validator 原文、参数值或 schema 内容。 +- 处理:使用完整 JSON Schema validator 校验原始 catalog schema,不手写 required/type 子集;native parser、fingerprint enrichment 与实际 MCP 调用边界复用同一校验器。enrichment 错误必须映射回 classified `arguments-schema` repair,不能以普通字符串直接终止 run;执行点重验用于阻断升级前已经落盘的 schema 外 pending。关闭网络和文件 `$ref` 解析,schema 无法安全编译时不广告或不执行。`serde` 类型错误会包含实际字符串值,catalog miss 也会包含模型提交的 server/tool,因此这两类错误同样只能返回稳定类别,不能拼接原始错误、参数值或 schema 内容。 - 验证:覆盖 required、additionalProperties、type、enum、本地 `$defs/$ref`、HTTP/file 外部引用、无效 schema、错误脱敏,证明 legacy wrapper 在注入 fingerprint 前进入 repair,并证明带旧有效 fingerprint 的历史 pending 在实际调用前仍被 schema 拒绝。