Skip to main content

pmcp_server/tools/
test_apps.rs

1//! MCP Apps metadata validation tool.
2//!
3//! Wraps `mcp_tester::AppValidator::validate_tools()` to validate that
4//! App-capable tools have correct `_meta` structure and resource cross-references.
5
6use 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/// Input parameters for the `test_apps` tool.
16#[derive(Deserialize)]
17struct TestAppsInput {
18    /// MCP server URL to validate.
19    url: String,
20    /// Timeout in seconds (default: 30).
21    #[serde(default = "default_timeout")]
22    timeout: u64,
23    /// Validation mode: "standard", "chatgpt", "claude", or "all".
24    #[serde(default = "default_mode")]
25    mode: String,
26    /// Filter to a single tool name.
27    #[serde(default)]
28    tool_filter: Option<String>,
29    /// Promote warnings to failures.
30    #[serde(default)]
31    strict: bool,
32}
33
34fn default_mode() -> String {
35    "standard".to_string()
36}
37
38/// Parse a mode string into one or more `AppValidationMode` values.
39///
40/// The "all" mode is not a single enum variant; instead it runs validation
41/// in every mode and combines the results.
42fn 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
58/// MCP Apps metadata validation tool.
59///
60/// Connects to a remote MCP server, discovers its tools and resources,
61/// then validates App metadata structure and cross-references.
62pub 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(&params.mode).map_err(pmcp::Error::validation)?;
71
72        let mut tester = create_tester(&params.url, params.timeout)?;
73
74        // Initialize and discover tools (run_quick_test only initializes).
75        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}