Skip to main content

sz_rust_capability/
builtin.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use serde_json::Value;
8
9use crate::capability::Capability;
10use crate::error::{CapError, CapResult};
11use crate::registry::CapabilityRegistry;
12use crate::source::CapabilitySource;
13
14pub struct McpCapabilityAdapter {
15    tool_name: &'static str,
16    cap_name: &'static str,
17    description: &'static str,
18    input_schema: Value,
19    tags: &'static [&'static str],
20}
21
22impl McpCapabilityAdapter {
23    pub fn new(tool_name: &'static str, input_schema: Value) -> Self {
24        Self {
25            tool_name,
26            cap_name: static_cap_name(tool_name),
27            description: static_description(tool_name),
28            input_schema,
29            tags: static_tags(tool_name),
30        }
31    }
32}
33
34#[async_trait]
35impl Capability for McpCapabilityAdapter {
36    fn name(&self) -> &'static str {
37        self.cap_name
38    }
39
40    fn description(&self) -> &'static str {
41        self.description
42    }
43
44    fn schema(&self) -> Value {
45        self.input_schema.clone()
46    }
47
48    fn tags(&self) -> &[&'static str] {
49        self.tags
50    }
51
52    fn source(&self) -> CapabilitySource {
53        CapabilitySource::Service
54    }
55
56    async fn call(&self, args: Value) -> CapResult<Value> {
57        let result = sz_rust_mcp::call_tool(self.tool_name, &args);
58        match result {
59            Ok(json_str) => serde_json::from_str(&json_str)
60                .map_err(|e| CapError::ExecutionError(format!("MCP 返回值 JSON 解析失败: {e}"))),
61            Err(sz_rust_mcp::McpError::ToolNotFound(name)) => Err(CapError::NotFound(name)),
62            Err(sz_rust_mcp::McpError::InvalidArguments(msg)) => {
63                Err(CapError::ValidationError(msg))
64            }
65            Err(sz_rust_mcp::McpError::Execution(msg)) => Err(CapError::ExecutionError(msg)),
66        }
67    }
68}
69
70fn static_cap_name(tool_name: &str) -> &'static str {
71    match tool_name {
72        "parse_path" => "mcp.parse_path",
73        "build_select_query" => "mcp.build_select_query",
74        "openapi_spec" => "mcp.openapi_spec",
75        "redaction_check" => "mcp.redaction_check",
76        "url_decode" => "mcp.url_decode",
77        "sql_validate" => "mcp.sql_validate",
78        "route_conflicts" => "mcp.route_conflicts",
79        "build_insert_query" => "mcp.build_insert_query",
80        "build_update_query" => "mcp.build_update_query",
81        "build_delete_query" => "mcp.build_delete_query",
82        "crud_read" => "mcp.crud_read",
83        "migrate_create" => "mcp.migrate_create",
84        "migrate_status" => "mcp.migrate_status",
85        "migrate_run" => "mcp.migrate_run",
86        "test_run" => "mcp.test_run",
87        "test_coverage" => "mcp.test_coverage",
88        "deploy_check" => "mcp.deploy_check",
89        "deploy_status" => "mcp.deploy_status",
90        "plugin_list" => "mcp.plugin_list",
91        "plugin_install" => "mcp.plugin_install",
92        "plugin_uninstall" => "mcp.plugin_uninstall",
93        _ => "mcp.unknown",
94    }
95}
96
97fn static_description(tool_name: &str) -> &'static str {
98    match tool_name {
99        "parse_path" => {
100            "解析 URI 为 (app, controller, action) 路由三元组(对齐 PHP auto_multi_app 规则)"
101        }
102        "build_select_query" => {
103            "构建参数化 SELECT 查询(显式列投影 + WHERE 绑定参数,防 SQL 注入)"
104        }
105        "openapi_spec" => "从路由配置自动生成 OpenAPI 3.0 spec",
106        "redaction_check" => {
107            "检查配置对象的 Debug 输出是否泄漏敏感字段(merchant_private_key 等应显示 <redacted>)"
108        }
109        "url_decode" => "URL 百分比解码(支持 UTF-8 多字节,对齐 PHP urldecode)",
110        "sql_validate" => "SQL 安全校验(注入防护:语句类型、危险模式、表名列名白名单校验)",
111        "route_conflicts" => "路由冲突检测:检查路由规则集合是否存在歧义/冲突",
112        "build_insert_query" => "构建参数化 INSERT 查询(防 SQL 注入)",
113        "build_update_query" => "构建参数化 UPDATE 查询(WHERE 绑定,防 SQL 注入)",
114        "build_delete_query" => "构建参数化 DELETE 查询(WHERE 绑定,防 SQL 注入)",
115        "crud_read" => "CRUD 读操作:构建参数化 SELECT 并返回 SQL + 参数数",
116        "migrate_create" => "生成迁移脚本模板(UP/DOWN SQL)",
117        "migrate_status" => "检查迁移状态:返回已执行/待执行迁移列表",
118        "migrate_run" => "生成执行迁移的命令(cargo run -p sz-rust-migration)",
119        "test_run" => "生成测试运行命令(cargo test)",
120        "test_coverage" => "生成覆盖率分析命令(cargo tarpaulin / cargo llvm-cov)",
121        "deploy_check" => "检查部署配置完整性(Docker/K8s 配置校验)",
122        "deploy_status" => "生成部署状态查询命令",
123        "plugin_list" => "列出已注册的 Capability(按 source/tags 过滤)",
124        "plugin_install" => "生成插件安装命令(cargo add + 配置注册)",
125        "plugin_uninstall" => "生成插件卸载命令(cargo remove + 清理配置)",
126        _ => "",
127    }
128}
129
130fn static_tags(tool_name: &str) -> &'static [&'static str] {
131    match tool_name {
132        "parse_path" => &["mcp", "router", "parse", "read"],
133        "build_select_query" => &["mcp", "orm", "query", "build"],
134        "openapi_spec" => &["mcp", "router", "openapi", "read"],
135        "redaction_check" => &["mcp", "security", "redaction", "read"],
136        "url_decode" => &["mcp", "http", "decode", "read"],
137        "sql_validate" => &["mcp", "orm", "security", "validate"],
138        "route_conflicts" => &["mcp", "router", "validate", "read"],
139        "build_insert_query" => &["mcp", "orm", "query", "write"],
140        "build_update_query" => &["mcp", "orm", "query", "write"],
141        "build_delete_query" => &["mcp", "orm", "query", "write"],
142        "crud_read" => &["mcp", "orm", "crud", "read"],
143        "migrate_create" => &["mcp", "migration", "create", "write"],
144        "migrate_status" => &["mcp", "migration", "status", "read"],
145        "migrate_run" => &["mcp", "migration", "run", "write"],
146
147        "test_run" => &["mcp", "test", "run", "read"],
148        "test_coverage" => &["mcp", "test", "coverage", "read"],
149        "deploy_check" => &["mcp", "deploy", "check", "read"],
150        "deploy_status" => &["mcp", "deploy", "status", "read"],
151        "plugin_list" => &["mcp", "plugin", "list", "read"],
152        "plugin_install" => &["mcp", "plugin", "install", "write"],
153        "plugin_uninstall" => &["mcp", "plugin", "uninstall", "write"],
154        _ => &["mcp"],
155    }
156}
157
158pub fn register_mcp_tools(registry: &CapabilityRegistry) -> CapResult<Vec<String>> {
159    let definitions = sz_rust_mcp::tool_definitions();
160    let mut registered = Vec::with_capacity(definitions.len());
161
162    for def in definitions {
163        let tool_name = def
164            .get("name")
165            .and_then(|v| v.as_str())
166            .ok_or_else(|| CapError::ExecutionError("MCP 工具定义缺少 name 字段".into()))?;
167
168        let input_schema = def
169            .get("inputSchema")
170            .cloned()
171            .unwrap_or_else(|| serde_json::json!({}));
172
173        let static_name = match tool_name {
174            "parse_path" => "parse_path",
175            "build_select_query" => "build_select_query",
176            "openapi_spec" => "openapi_spec",
177            "redaction_check" => "redaction_check",
178            "url_decode" => "url_decode",
179            "sql_validate" => "sql_validate",
180            "route_conflicts" => "route_conflicts",
181            "build_insert_query" => "build_insert_query",
182            "build_update_query" => "build_update_query",
183            "build_delete_query" => "build_delete_query",
184            "crud_read" => "crud_read",
185            "migrate_create" => "migrate_create",
186            "migrate_status" => "migrate_status",
187            "migrate_run" => "migrate_run",
188            "test_run" => "test_run",
189            "test_coverage" => "test_coverage",
190            "deploy_check" => "deploy_check",
191            "deploy_status" => "deploy_status",
192            "plugin_list" => "plugin_list",
193            "plugin_install" => "plugin_install",
194            "plugin_uninstall" => "plugin_uninstall",
195            other => return Err(CapError::ExecutionError(format!("未知 MCP 工具: {other}"))),
196        };
197
198        let adapter =
199            Arc::new(McpCapabilityAdapter::new(static_name, input_schema)) as Arc<dyn Capability>;
200        registry.register(adapter);
201
202        registered.push(format!("mcp.{static_name}"));
203    }
204
205    Ok(registered)
206}
207
208pub fn register_builtin_skills(
209    registry: &CapabilityRegistry,
210    skills: Vec<Arc<dyn Capability>>,
211) -> CapResult<Vec<String>> {
212    let mut registered = Vec::with_capacity(skills.len());
213    for skill in skills {
214        let name = skill.name().to_string();
215        registry.register(skill);
216        registered.push(name);
217    }
218    Ok(registered)
219}
220
221/// 扩展 MCP 工具适配器 — 将 `McpTool` trait 适配为 `Capability`。
222pub struct ExtendedMcpAdapter {
223    tool: Box<dyn sz_rust_mcp::tool::McpTool>,
224}
225
226impl ExtendedMcpAdapter {
227    pub fn new(tool: Box<dyn sz_rust_mcp::tool::McpTool>) -> Self {
228        Self { tool }
229    }
230}
231
232#[async_trait]
233impl Capability for ExtendedMcpAdapter {
234    fn name(&self) -> &'static str {
235        extended_cap_name(self.tool.name())
236    }
237    fn description(&self) -> &'static str {
238        extended_description(self.tool.name())
239    }
240    fn schema(&self) -> Value {
241        self.tool.input_schema()
242    }
243    fn tags(&self) -> &[&'static str] {
244        extended_tags(self.tool.name())
245    }
246    fn source(&self) -> CapabilitySource {
247        CapabilitySource::Skill
248    }
249    async fn call(&self, args: Value) -> CapResult<Value> {
250        self.tool.execute(args).await.map_err(|e| match e {
251            sz_rust_mcp::tool::ToolError::InvalidArgs(msg) => CapError::ValidationError(msg),
252            sz_rust_mcp::tool::ToolError::ExecutionFailed(msg) => CapError::ExecutionError(msg),
253            sz_rust_mcp::tool::ToolError::PermissionDenied(msg) => CapError::ValidationError(msg),
254            sz_rust_mcp::tool::ToolError::ConfirmationRequired => {
255                CapError::ValidationError("需要人工确认".into())
256            }
257            sz_rust_mcp::tool::ToolError::Timeout(msg) => CapError::ExecutionError(msg),
258        })
259    }
260    fn requires_confirmation(&self) -> bool {
261        self.tool.requires_confirmation()
262    }
263}
264
265fn extended_cap_name(tool_name: &str) -> &'static str {
266    match tool_name {
267        "crud_create" => "mcp.crud_create",
268        "crud_read" => "mcp.crud_read",
269        "crud_update" => "mcp.crud_update",
270        "crud_delete" => "mcp.crud_delete",
271        "migrate_create" => "mcp.migrate_create",
272        "migrate_run" => "mcp.migrate_run",
273        "test_run" => "mcp.test_run",
274        "deploy_run" => "mcp.deploy_run",
275        "plugin_install" => "mcp.plugin_install",
276        "plugin_uninstall" => "mcp.plugin_uninstall",
277        _ => "mcp.unknown",
278    }
279}
280
281fn extended_description(tool_name: &str) -> &'static str {
282    match tool_name {
283        "crud_create" => "通过 CapabilityRegistry 创建资源",
284        "crud_read" => "通过 CapabilityRegistry 查询资源",
285        "crud_update" => "通过 CapabilityRegistry 更新资源",
286        "crud_delete" => "通过 CapabilityRegistry 删除资源(需要确认)",
287        "migrate_create" => "生成迁移脚本模板(UP/DOWN SQL),使用 tokio::fs 写文件",
288        "migrate_run" => "执行迁移(cargo run -p sz-rust-migration)",
289        "test_run" => "异步执行 cargo test,返回 passed/failed/skipped 数量",
290        "deploy_run" => "通过 Node.js ssh2 包执行远程部署(需要确认)",
291        "plugin_install" => "从插件市场安装插件(cargo add + 注册到 CapabilityRegistry)",
292        "plugin_uninstall" => "卸载插件(cargo remove + 清理注册,需要确认)",
293        _ => "",
294    }
295}
296
297fn extended_tags(tool_name: &str) -> &'static [&'static str] {
298    match tool_name {
299        "crud_create" => &["mcp", "crud", "create", "write"],
300        "crud_read" => &["mcp", "crud", "read", "read"],
301        "crud_update" => &["mcp", "crud", "update", "write"],
302        "crud_delete" => &["mcp", "crud", "delete", "write"],
303        "migrate_create" => &["mcp", "migration", "create", "write"],
304        "migrate_run" => &["mcp", "migration", "run", "write"],
305        "test_run" => &["mcp", "test", "run", "read"],
306        "deploy_run" => &["mcp", "deploy", "run", "write"],
307        "plugin_install" => &["mcp", "plugin", "install", "write"],
308        "plugin_uninstall" => &["mcp", "plugin", "uninstall", "write"],
309        _ => &["mcp"],
310    }
311}
312
313/// 将扩展 MCP 工具(基于 McpTool trait)注册到 CapabilityRegistry。
314pub fn register_extended_mcp_tools(registry: &CapabilityRegistry) -> CapResult<Vec<String>> {
315    let tools = sz_rust_mcp::extended_tools();
316    let mut registered = Vec::with_capacity(tools.len());
317
318    for tool in tools {
319        let cap_name = extended_cap_name(tool.name()).to_string();
320        let adapter = Arc::new(ExtendedMcpAdapter::new(tool)) as Arc<dyn Capability>;
321        registry.register(adapter);
322        registered.push(cap_name);
323    }
324
325    Ok(registered)
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use serde_json::json;
332
333    #[test]
334    fn test_register_mcp_tools() {
335        let registry = CapabilityRegistry::new();
336        let result = register_mcp_tools(&registry);
337        assert!(result.is_ok());
338        let names = result.expect("register_builtin_skills 应成功(前一行已断言 is_ok)");
339        assert_eq!(names.len(), 21);
340        assert!(names.contains(&"mcp.parse_path".to_string()));
341        assert!(names.contains(&"mcp.sql_validate".to_string()));
342        assert!(names.contains(&"mcp.build_insert_query".to_string()));
343        assert!(names.contains(&"mcp.plugin_list".to_string()));
344        assert_eq!(registry.list_by_source(CapabilitySource::Service).len(), 21);
345    }
346
347    #[test]
348    fn test_mcp_adapter_tags() {
349        let registry = CapabilityRegistry::new();
350        register_mcp_tools(&registry).unwrap();
351        let caps = registry.find_by_tags(&["mcp", "router"], None);
352        assert_eq!(caps.len(), 3);
353        let caps = registry.find_by_tags(&["mcp", "orm"], None);
354        assert_eq!(caps.len(), 6);
355        let caps = registry.find_by_tags(&["mcp", "security"], None);
356        assert_eq!(caps.len(), 2);
357        let caps = registry.find_by_tags(&["mcp", "migration"], None);
358        assert_eq!(caps.len(), 3);
359        let caps = registry.find_by_tags(&["mcp", "plugin"], None);
360        assert_eq!(caps.len(), 3);
361    }
362
363    #[tokio::test]
364    async fn test_mcp_adapter_call_url_decode() {
365        let registry = CapabilityRegistry::new();
366        register_mcp_tools(&registry).unwrap();
367        let result = registry
368            .call("mcp.url_decode", json!({"value": "%E4%BD%A0%E5%A5%BD"}))
369            .await;
370        assert!(result.is_ok());
371    }
372
373    #[tokio::test]
374    async fn test_mcp_adapter_call_not_found() {
375        let registry = CapabilityRegistry::new();
376        register_mcp_tools(&registry).unwrap();
377        let result = registry.call("mcp.nonexistent", json!({})).await;
378        assert!(matches!(result, Err(CapError::NotFound(_))));
379    }
380
381    #[test]
382    fn test_register_builtin_skills() {
383        struct DummySkill;
384        #[async_trait]
385        impl Capability for DummySkill {
386            fn name(&self) -> &'static str {
387                "dummy_skill"
388            }
389            fn description(&self) -> &'static str {
390                "测试 Skill"
391            }
392            fn schema(&self) -> Value {
393                json!({})
394            }
395            fn tags(&self) -> &[&'static str] {
396                &["test"]
397            }
398            fn source(&self) -> CapabilitySource {
399                CapabilitySource::Skill
400            }
401            async fn call(&self, _args: Value) -> CapResult<Value> {
402                Ok(json!({}))
403            }
404        }
405
406        let registry = CapabilityRegistry::new();
407        let skills = vec![Arc::new(DummySkill) as Arc<dyn Capability>];
408        let result = register_builtin_skills(&registry, skills);
409        let names = result.expect("register_builtin_skills 应成功");
410        assert_eq!(names, vec!["dummy_skill"]);
411        assert_eq!(registry.list_by_source(CapabilitySource::Skill).len(), 1);
412    }
413
414    #[test]
415    fn test_register_extended_mcp_tools() {
416        let registry = CapabilityRegistry::new();
417        let result = register_extended_mcp_tools(&registry);
418        assert!(result.is_ok());
419        let names = result.expect("register_builtin_skills 应成功(前一行已断言 is_ok)");
420        assert_eq!(names.len(), 10, "应注册 10 个扩展工具");
421        assert!(names.contains(&"mcp.crud_create".to_string()));
422        assert!(names.contains(&"mcp.crud_read".to_string()));
423        assert!(names.contains(&"mcp.crud_update".to_string()));
424        assert!(names.contains(&"mcp.crud_delete".to_string()));
425        assert!(names.contains(&"mcp.migrate_create".to_string()));
426        assert!(names.contains(&"mcp.migrate_run".to_string()));
427        assert!(names.contains(&"mcp.test_run".to_string()));
428        assert!(names.contains(&"mcp.deploy_run".to_string()));
429        assert!(names.contains(&"mcp.plugin_install".to_string()));
430        assert!(names.contains(&"mcp.plugin_uninstall".to_string()));
431        assert_eq!(registry.list_by_source(CapabilitySource::Skill).len(), 10);
432    }
433
434    #[test]
435    fn test_extended_mcp_tags() {
436        let registry = CapabilityRegistry::new();
437        register_extended_mcp_tools(&registry).unwrap();
438        let caps = registry.find_by_tags(&["mcp", "crud"], None);
439        assert_eq!(caps.len(), 4, "应有 4 个 CRUD 工具");
440        let caps = registry.find_by_tags(&["mcp", "migration"], None);
441        assert_eq!(caps.len(), 2, "应有 2 个迁移工具");
442        let caps = registry.find_by_tags(&["mcp", "deploy"], None);
443        assert_eq!(caps.len(), 1, "应有 1 个部署工具");
444        let caps = registry.find_by_tags(&["mcp", "plugin"], None);
445        assert_eq!(caps.len(), 2, "应有 2 个插件工具");
446    }
447
448    #[test]
449    fn test_extended_mcp_confirmation() {
450        let registry = CapabilityRegistry::new();
451        register_extended_mcp_tools(&registry).unwrap();
452        let delete_cap = registry.get("mcp.crud_delete").unwrap();
453        assert!(delete_cap.requires_confirmation());
454        let deploy_cap = registry.get("mcp.deploy_run").unwrap();
455        assert!(deploy_cap.requires_confirmation());
456        let uninstall_cap = registry.get("mcp.plugin_uninstall").unwrap();
457        assert!(uninstall_cap.requires_confirmation());
458        let create_cap = registry.get("mcp.crud_create").unwrap();
459        assert!(!create_cap.requires_confirmation());
460    }
461
462    #[tokio::test]
463    async fn test_extended_mcp_call_crud_create() {
464        let registry = CapabilityRegistry::new();
465        register_extended_mcp_tools(&registry).unwrap();
466        let result = registry
467            .call(
468                "mcp.crud_create",
469                json!({"capability": "test_cap", "data": {}, "tenant_id": 1}),
470            )
471            .await;
472        assert!(result.is_ok());
473        let out = result.unwrap();
474        assert_eq!(out["status"], "created");
475    }
476}