修正MCP语义工具的破坏性风险注解
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m27s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m0s
Project CI / Backend tests (pull_request) Successful in 3m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m4s
Project CI / Native shell tests (pull_request) Successful in 6m4s
Project CI / Frontend tests (pull_request) Successful in 2m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m10s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m52s
Project CI / Repository checks (pull_request) Successful in 2m11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m27s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m0s
Project CI / Backend tests (pull_request) Successful in 3m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m4s
Project CI / Native shell tests (pull_request) Successful in 6m4s
Project CI / Frontend tests (pull_request) Successful in 2m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m10s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m52s
Project CI / Repository checks (pull_request) Successful in 2m11s
按 API 操作显式声明风险并汇总到工具级注解 修正上传确认可覆盖已有对象元数据的漏标 增加工具风险注解回归测试并同步工程文档
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
外部 MCP 语义工具设计:
|
||||
|
||||
1. [外部 OpenAPI 与 API Key 接入方案](../../【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md):现役托管 MCP 与 External API 合同。
|
||||
2. [外部 MCP 语义工具说明与参数设计](../../technical/【技术方案】外部MCP语义工具说明与参数设计-2026-09-23.md):15 个语义工具与全部旧工具并存,复用现有 API 分派和 schema;多功能入口使用 action/input,结果不裁剪,可选幂等只扩展新入口。instructions 与 resources 已同步当前工具选择、调用流程及结果说明;资源 URI 保留,Skill 文档源与下载包共用,线上状态按实际部署核对。
|
||||
2. [外部 MCP 语义工具说明与参数设计](../../technical/【技术方案】外部MCP语义工具说明与参数设计-2026-09-23.md):15 个语义工具与全部旧工具并存,复用现有 API 分派和 schema;多功能入口使用 action/input,结果不裁剪,可选幂等只扩展新入口。语义工具按 operation 显式声明 destructive 风险,并汇总各 action;上传确认包含已有元数据更新风险。instructions 与 resources 已同步当前工具选择、调用流程及结果说明;资源 URI 保留,Skill 文档源与下载包共用,线上状态按实际部署核对。
|
||||
|
||||
AI 游戏创作 / DirectProject / UI workflow:
|
||||
|
||||
|
||||
@@ -321,6 +321,7 @@ register_resource 只登记资源,并不自动创建画布图层。正常生
|
||||
- variation 不暴露 kind,固定注入 `quick-edit`;对象确认不暴露或接受 ownerUserId。其余公开字段全部保留。稳定 key、原 API 与规范请求保持一致,重复语义入口不新增幂等命名空间。
|
||||
- 项目列表固定 summary。get_project_resources 返回完整项目响应,不裁剪;下载地址保留 objectKey、legacyPublicPath 与 expireSeconds。所有结果复用现有成功解包与结构化错误处理,不裁剪告警、revision 冲突或异步结果。
|
||||
- 读取工具标记 readOnly;含删除、覆盖或移动已有状态的工具标记 destructive;生成类说明付费及异步语义,可能替换已有图层的生成工具也按潜在破坏性标记。openWorld 仅用于会访问外部服务的能力,不作为付费标志。注解不替代用户授权或后端校验。
|
||||
- 语义工具的 destructive 按 API operation 显式声明,并取各 action 风险的并集,不根据 HTTP 方法或参数字段名推断。`prepare_asset_upload/confirm_upload` 可以更新同一 owner 的已有对象元数据,因此整个上传工具标记 destructive;申请上传票据本身仍属于新增操作。新增 operation 必须补充风险声明,原有工具的注解保持兼容。
|
||||
|
||||
### 5.3 验收
|
||||
|
||||
@@ -332,8 +333,8 @@ register_resource 只登记资源,并不自动创建画布图层。正常生
|
||||
|
||||
| 验证范围 | 证据与结果 |
|
||||
| --- | --- |
|
||||
| 工具与参数合同 | `cargo test --locked -p api-server external_mcp`:24 项通过,覆盖 44 个工具、31 个 action、旧定义保留、schema 展开、参数映射、错分支拒绝、必填/可选幂等和等价入口 |
|
||||
| 认证与既有 API 回归 | 2026-09-24 在包含最新 master、instructions 和 resources 更新的分支上运行 `cargo test --locked -p api-server external_`:155 项通过,含 MCP 内外认证、scope 拒绝、跨 owner 隔离、异步及 OpenAPI 回归 |
|
||||
| 工具与参数合同 | `external_` 回归中的 25 项 `external_mcp` 测试通过,覆盖 44 个工具、31 个 action、旧定义保留、schema 展开、参数映射、错分支拒绝、必填/可选幂等、等价入口及 15 个语义工具的风险注解 |
|
||||
| 认证与既有 API 回归 | 2026-09-24 在包含最新 master、instructions、resources 和风险注解修复的分支上运行 `cargo test --locked -p api-server external_`:156 项通过,含 MCP 内外认证、scope 拒绝、跨 owner 隔离、异步及 OpenAPI 回归 |
|
||||
| 编译与文本检查 | `cargo check --locked -p api-server`、定向 rustfmt、文档索引、编码和 diff 检查通过 |
|
||||
| 本地服务启动 | 先通过 `npm run dev:spacetime` 启动 SpacetimeDB 2.8.3 并发布隔离数据库,再通过 `npm run dev:api-server` 启动同一目标的 API 和 worker;`/v1/ping`、`/healthz`、`/readyz` 均返回 200 |
|
||||
| MCP 真实 HTTP 链路 | 未认证返回 401;使用隔离数据库中的临时测试 API Key,initialize 成功,tools/list 返回 44 个工具且包含全部 29 个旧工具,resources/list 返回原有 7 个资源 |
|
||||
|
||||
@@ -16,6 +16,7 @@ struct Action {
|
||||
input_schema: Value,
|
||||
key_schema: Option<Value>,
|
||||
fixed_body: Map<String, Value>,
|
||||
destructive: bool,
|
||||
}
|
||||
|
||||
pub(super) struct PreparedCall {
|
||||
@@ -135,14 +136,7 @@ fn build_tools() -> Vec<SemanticTool> {
|
||||
.collect::<Vec<_>>();
|
||||
let read_only = actions.iter().all(|a| a.operation.method == Method::GET);
|
||||
let generation = actions.iter().any(|a| a.operation.requires_idempotency_key);
|
||||
let destructive = actions.iter().any(|a| {
|
||||
a.operation.method == Method::DELETE
|
||||
|| a.operation.method == Method::PATCH
|
||||
|| a.input_schema["properties"]
|
||||
.get("canvasCompletion")
|
||||
.is_some()
|
||||
|| a.input_schema["properties"].get("targetLayerId").is_some()
|
||||
});
|
||||
let destructive = actions.iter().any(|a| a.destructive);
|
||||
let schema = tool_schema(&actions);
|
||||
let mut tool = Tool::new(
|
||||
name.to_string(),
|
||||
@@ -176,6 +170,40 @@ fn build_tools() -> Vec<SemanticTool> {
|
||||
|
||||
impl Action {
|
||||
fn new(name: Option<&'static str>, operation_id: &str, openapi: &Value) -> Self {
|
||||
// 按实际副作用声明;POST 也可能覆盖已有记录,参数名不能代表风险。
|
||||
let destructive = match operation_id {
|
||||
"listEditorProjects"
|
||||
| "loadRecentEditorProject"
|
||||
| "getEditorProject"
|
||||
| "getEditorAssetLibrary"
|
||||
| "getExternalAssetReadUrl"
|
||||
| "getExternalEditorGenerationJob"
|
||||
| "createEditorProject"
|
||||
| "createEditorProjectResource"
|
||||
| "createEditorAssetFolder"
|
||||
| "createEditorAsset"
|
||||
| "createExternalDirectUploadTicket" => false,
|
||||
// 对象确认允许更新同一 owner 的已有对象元数据。
|
||||
"confirmExternalAssetObject"
|
||||
| "renameEditorProject"
|
||||
| "saveEditorProjectCanvas"
|
||||
| "updateEditorAssetFolder"
|
||||
| "updateEditorAsset"
|
||||
| "deleteEditorProject"
|
||||
| "deleteEditorAssetFolder"
|
||||
| "deleteEditorAsset" => true,
|
||||
// 生成完成可修改已有画布状态,编辑与抠图还支持原位替换。
|
||||
"generateExternalEditorImage"
|
||||
| "editExternalEditorImage"
|
||||
| "removeExternalEditorImageBackground"
|
||||
| "generateExternalEditorIconSpritesheet"
|
||||
| "extractExternalEditorUiDesignAssets"
|
||||
| "generateExternalEditorCharacterAnimation"
|
||||
| "generateExternalEditorVideo"
|
||||
| "generateExternalEditorSoundEffect"
|
||||
| "generateExternalEditorBackgroundMusic" => true,
|
||||
_ => panic!("semantic operation must declare destructive risk: {operation_id}"),
|
||||
};
|
||||
let operation = MCP_OPERATIONS
|
||||
.iter()
|
||||
.find(|op| op.operation_id == operation_id)
|
||||
@@ -259,6 +287,7 @@ impl Action {
|
||||
input_schema,
|
||||
key_schema,
|
||||
fixed_body,
|
||||
destructive,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,50 @@ fn semantic_catalog_adds_fifteen_tools_without_replacing_legacy_tools() {
|
||||
assert_eq!(names.len(), 44);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_annotations_cover_existing_state_changes_in_any_action() {
|
||||
for (name, destructive) in [
|
||||
("find_canvas_projects", false),
|
||||
("find_assets", false),
|
||||
("check_generation", false),
|
||||
("manage_canvas_projects", true),
|
||||
("prepare_asset_upload", true),
|
||||
("generate_image", true),
|
||||
("modify_image", true),
|
||||
("generate_icon_spritesheet", true),
|
||||
("extract_ui_assets", true),
|
||||
("generate_character_animation", true),
|
||||
("generate_video", true),
|
||||
("generate_audio", true),
|
||||
("edit_canvas", true),
|
||||
("organize_asset_library", true),
|
||||
("delete_resources", true),
|
||||
] {
|
||||
let tool = &find(name).expect("semantic tool must exist").tool;
|
||||
let serialized = serde_json::to_value(tool).unwrap();
|
||||
assert_eq!(
|
||||
serialized["annotations"]["destructiveHint"],
|
||||
json!(destructive),
|
||||
"incorrect published risk for {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// 申请票据只新增;确认对象会 upsert,整个工具必须涵盖该分支的风险。
|
||||
let upload = find("prepare_asset_upload").unwrap();
|
||||
let ticket = upload
|
||||
.actions
|
||||
.iter()
|
||||
.find(|a| a.name == Some("create_upload_ticket"))
|
||||
.unwrap();
|
||||
let confirm = upload
|
||||
.actions
|
||||
.iter()
|
||||
.find(|a| a.name == Some("confirm_upload"))
|
||||
.unwrap();
|
||||
assert!(!ticket.destructive);
|
||||
assert!(confirm.destructive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_semantic_action_maps_to_one_existing_operation_and_correct_parameter_location() {
|
||||
// (tool, action, operationId, input, pathParameters, queryParameters, body)
|
||||
|
||||
Reference in New Issue
Block a user