pmcp_server/tools/
schema_export.rs1use async_trait::async_trait;
7use pmcp::types::ToolInfo;
8use pmcp::ToolHandler;
9use serde::Deserialize;
10use serde_json::{json, Value};
11
12use super::{create_tester, default_timeout, internal_err};
13use crate::util::to_pascal_case;
14
15#[derive(Deserialize)]
17struct SchemaExportInput {
18 url: String,
20 #[serde(default = "default_format")]
22 format: String,
23 #[serde(default = "default_timeout")]
25 timeout: u64,
26}
27
28fn default_format() -> String {
29 "json".to_string()
30}
31
32pub struct SchemaExportTool;
37
38#[async_trait]
39impl ToolHandler for SchemaExportTool {
40 async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
41 let params: SchemaExportInput = serde_json::from_value(args)
42 .map_err(|e| pmcp::Error::validation(format!("Invalid arguments: {e}")))?;
43
44 if params.format != "json" && params.format != "rust" {
45 return Err(pmcp::Error::validation(format!(
46 "Unknown format '{}'. Available: json, rust",
47 params.format
48 )));
49 }
50
51 let mut tester = create_tester(¶ms.url, params.timeout)?;
52
53 tester.run_quick_test().await.map_err(internal_err)?;
55
56 let tools_result = tester.test_tools_list().await;
58 if tools_result.status == mcp_tester::TestStatus::Failed {
59 return Err(internal_err(
60 tools_result
61 .error
62 .unwrap_or_else(|| "failed to list tools".into()),
63 ));
64 }
65
66 let server_name = tester
67 .get_server_name()
68 .unwrap_or_else(|| "unknown".to_string());
69 let server_version = tester
70 .get_server_version()
71 .unwrap_or_else(|| "unknown".to_string());
72
73 let mut response = json!({
74 "server_name": server_name,
75 "server_version": server_version,
76 "format": params.format,
77 });
78
79 if params.format == "json" {
80 let tools_value: Value = match tester.get_tools() {
82 Some(tools) => serde_json::to_value(tools).map_err(internal_err)?,
83 None => json!([]),
84 };
85 let resources_value: Value = match tester.list_resources().await {
86 Ok(res) => serde_json::to_value(&res.resources).map_err(internal_err)?,
87 Err(_) => json!([]),
88 };
89 let prompts_value: Value = match tester.list_prompts().await {
90 Ok(res) => serde_json::to_value(&res.prompts).map_err(internal_err)?,
91 Err(_) => json!([]),
92 };
93 response["tools"] = tools_value;
94 response["resources"] = resources_value;
95 response["prompts"] = prompts_value;
96 } else {
97 let rust_types = generate_rust_types(tester.get_tools());
99 response["rust_types"] = json!(rust_types);
100 }
101
102 Ok(response)
103 }
104
105 fn metadata(&self) -> Option<ToolInfo> {
106 Some(ToolInfo::new(
107 "schema_export",
108 Some(
109 "Connect to a remote MCP server and export its tool/resource/prompt schemas"
110 .to_string(),
111 ),
112 json!({
113 "type": "object",
114 "properties": {
115 "url": {
116 "type": "string",
117 "description": "MCP server URL to export schemas from"
118 },
119 "format": {
120 "type": "string",
121 "enum": ["json", "rust"],
122 "description": "Output format (default: json)",
123 "default": "json"
124 },
125 "timeout": {
126 "type": "integer",
127 "description": "Timeout in seconds",
128 "default": 30
129 }
130 },
131 "required": ["url"]
132 }),
133 ))
134 }
135}
136
137fn json_type_to_rust(json_type: &str) -> &str {
143 match json_type {
144 "string" => "String",
145 "number" => "f64",
146 "integer" => "i64",
147 "boolean" => "bool",
148 "array" => "Vec<serde_json::Value>",
149 "object" => "serde_json::Value",
150 _ => "serde_json::Value",
151 }
152}
153
154fn generate_rust_types(tools: Option<&Vec<pmcp::types::ToolInfo>>) -> String {
156 let tools = match tools {
157 Some(t) if !t.is_empty() => t,
158 _ => return "// No tools discovered -- no types to generate.\n".to_string(),
159 };
160
161 let mut output = String::from("use serde::Deserialize;\n\n");
162
163 for tool in tools {
164 let struct_name = format!("{}Input", to_pascal_case(&tool.name));
165 let properties = tool
166 .input_schema
167 .get("properties")
168 .and_then(|p| p.as_object());
169
170 let required: Vec<&str> = tool
171 .input_schema
172 .get("required")
173 .and_then(|r| r.as_array())
174 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
175 .unwrap_or_default();
176
177 let has_camel_case = properties
179 .map(|props| props.keys().any(|k| k.chars().any(|c| c.is_uppercase())))
180 .unwrap_or(false);
181
182 output.push_str("#[derive(Deserialize)]\n");
183 if has_camel_case {
184 output.push_str("#[serde(rename_all = \"camelCase\")]\n");
185 }
186 output.push_str(&format!("pub struct {struct_name} {{\n"));
187
188 if let Some(props) = properties {
189 for (field_name, field_schema) in props {
190 let field_type_str = field_schema
191 .get("type")
192 .and_then(|t| t.as_str())
193 .unwrap_or("object");
194 let rust_type = json_type_to_rust(field_type_str);
195
196 if let Some(desc) = field_schema.get("description").and_then(|d| d.as_str()) {
198 output.push_str(&format!(" /// {desc}\n"));
199 }
200
201 let is_required = required.contains(&field_name.as_str());
202 if is_required {
203 output.push_str(&format!(" pub {field_name}: {rust_type},\n"));
204 } else {
205 output.push_str(&format!(" pub {field_name}: Option<{rust_type}>,\n"));
206 }
207 }
208 }
209
210 output.push_str("}\n\n");
211 }
212
213 output
214}