1use 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
221pub 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
313pub 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(®istry);
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(®istry).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(®istry).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(®istry).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(®istry, 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(®istry);
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(®istry).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(®istry).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(®istry).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}