Skip to main content

sz_rust_capability/
builtin.rs

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