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
218pub 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
310pub 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(®istry);
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(®istry).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(®istry).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(®istry).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(®istry, 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(®istry);
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(®istry).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(®istry).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(®istry).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}