pmcp_server/tools/
test_apps.rs1use async_trait::async_trait;
7use mcp_tester::{AppValidationMode, AppValidator};
8use pmcp::types::ToolInfo;
9use pmcp::ToolHandler;
10use serde::Deserialize;
11use serde_json::{json, Value};
12
13use super::{create_tester, default_timeout, internal_err};
14
15#[derive(Deserialize)]
17struct TestAppsInput {
18 url: String,
20 #[serde(default = "default_timeout")]
22 timeout: u64,
23 #[serde(default = "default_mode")]
25 mode: String,
26 #[serde(default)]
28 tool_filter: Option<String>,
29 #[serde(default)]
31 strict: bool,
32}
33
34fn default_mode() -> String {
35 "standard".to_string()
36}
37
38fn parse_modes(mode: &str) -> Result<Vec<AppValidationMode>, String> {
43 match mode {
44 "standard" => Ok(vec![AppValidationMode::Standard]),
45 "chatgpt" => Ok(vec![AppValidationMode::ChatGpt]),
46 "claude" | "claude-desktop" => Ok(vec![AppValidationMode::ClaudeDesktop]),
47 "all" => Ok(vec![
48 AppValidationMode::Standard,
49 AppValidationMode::ChatGpt,
50 AppValidationMode::ClaudeDesktop,
51 ]),
52 other => Err(format!(
53 "Unknown validation mode: '{other}'. Valid: standard, chatgpt, claude, all"
54 )),
55 }
56}
57
58pub struct TestAppsTool;
63
64#[async_trait]
65impl ToolHandler for TestAppsTool {
66 async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
67 let params: TestAppsInput = serde_json::from_value(args)
68 .map_err(|e| pmcp::Error::validation(format!("Invalid arguments: {e}")))?;
69
70 let modes = parse_modes(¶ms.mode).map_err(pmcp::Error::validation)?;
71
72 let mut tester = create_tester(¶ms.url, params.timeout)?;
73
74 tester.run_quick_test().await.map_err(internal_err)?;
76 let tools_result = tester.test_tools_list().await;
77 if tools_result.status == mcp_tester::TestStatus::Failed {
78 return Err(internal_err(
79 tools_result
80 .error
81 .unwrap_or_else(|| "failed to list tools".into()),
82 ));
83 }
84
85 let tools = tester.get_tools().cloned().unwrap_or_default();
86 let resources = tester
87 .list_resources()
88 .await
89 .map(|r| r.resources)
90 .unwrap_or_default();
91
92 let mut all_results = Vec::new();
93 let tool_filter = params.tool_filter;
94 for mode in modes {
95 let validator = AppValidator::new(mode, tool_filter.clone());
96 let mut results = validator.validate_tools(&tools, &resources);
97
98 if params.strict {
99 for r in &mut results {
100 if r.status == mcp_tester::TestStatus::Warning {
101 r.status = mcp_tester::TestStatus::Failed;
102 }
103 }
104 }
105
106 all_results.extend(results);
107 }
108
109 serde_json::to_value(&all_results).map_err(internal_err)
110 }
111
112 fn metadata(&self) -> Option<ToolInfo> {
113 Some(ToolInfo::new(
114 "test_apps",
115 Some("Validate MCP Apps metadata on a remote server's tools".to_string()),
116 json!({
117 "type": "object",
118 "properties": {
119 "url": {
120 "type": "string",
121 "description": "MCP server URL to validate"
122 },
123 "timeout": {
124 "type": "integer",
125 "description": "Timeout in seconds",
126 "default": 30
127 },
128 "mode": {
129 "type": "string",
130 "description": "Validation mode",
131 "enum": ["standard", "chatgpt", "claude", "all"],
132 "default": "standard"
133 },
134 "tool_filter": {
135 "type": "string",
136 "description": "Filter to a single tool name"
137 },
138 "strict": {
139 "type": "boolean",
140 "description": "Promote warnings to failures",
141 "default": false
142 }
143 },
144 "required": ["url"]
145 }),
146 ))
147 }
148}