1use std::collections::BTreeMap;
19
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28pub struct Tool {
29 pub name: String,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub description: Option<String>,
34 #[serde(rename = "inputSchema", default)]
37 pub input_schema: serde_json::Value,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub action: Option<String>,
42 #[serde(default)]
45 pub autosubmit: bool,
46 #[serde(flatten)]
49 pub extra: BTreeMap<String, serde_json::Value>,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
55pub struct ListToolsParams {}
56
57impl ListToolsParams {
58 pub const IDENTIFIER: &'static str = "WebMCP.listTools";
59}
60
61impl chromiumoxide_types::Method for ListToolsParams {
62 fn identifier(&self) -> chromiumoxide_types::MethodId {
63 Self::IDENTIFIER.into()
64 }
65}
66
67impl chromiumoxide_types::MethodType for ListToolsParams {
68 fn method_id() -> chromiumoxide_types::MethodId
69 where
70 Self: Sized,
71 {
72 Self::IDENTIFIER.into()
73 }
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
78pub struct ListToolsReturns {
79 #[serde(default)]
81 pub tools: Vec<Tool>,
82}
83
84impl chromiumoxide_types::Command for ListToolsParams {
85 type Response = ListToolsReturns;
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
103pub struct CallToolParams {
104 pub name: String,
106 #[serde(default)]
108 pub arguments: serde_json::Value,
109}
110
111impl CallToolParams {
112 pub const IDENTIFIER: &'static str = "WebMCP.callTool";
113
114 pub fn new(name: impl Into<String>, arguments: serde_json::Value) -> Self {
116 Self {
117 name: name.into(),
118 arguments,
119 }
120 }
121}
122
123impl chromiumoxide_types::Method for CallToolParams {
124 fn identifier(&self) -> chromiumoxide_types::MethodId {
125 Self::IDENTIFIER.into()
126 }
127}
128
129impl chromiumoxide_types::MethodType for CallToolParams {
130 fn method_id() -> chromiumoxide_types::MethodId
131 where
132 Self: Sized,
133 {
134 Self::IDENTIFIER.into()
135 }
136}
137
138impl chromiumoxide_types::Command for CallToolParams {
139 type Response = serde_json::Value;
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use serde_json::json;
146
147 const SAMPLE: &str = r#"{ "tools": [ {
148 "name": "createSupportRequest",
149 "description": "File a support ticket",
150 "inputSchema": { "type": "object",
151 "properties": { "subject": { "type": "string", "description": "Short summary" },
152 "urgent": { "type": "boolean" },
153 "category": { "type": "string", "enum": ["billing","tech"] } },
154 "required": ["subject"] },
155 "source": "declarative",
156 "action": "/support",
157 "autosubmit": true,
158 "integrity": { "trusted": false, "confidence": 0.7,
159 "hints": { "readOnly": false, "untrustedContent": true } }
160 } ] }"#;
161
162 #[test]
163 fn deserializes_list_tools_sample_payload() {
164 let returns: ListToolsReturns =
165 serde_json::from_str(SAMPLE).expect("sample must deserialize");
166 assert_eq!(returns.tools.len(), 1);
167 let tool = &returns.tools[0];
168
169 assert_eq!(tool.name, "createSupportRequest");
171 assert_eq!(tool.description.as_deref(), Some("File a support ticket"));
172 assert_eq!(tool.action.as_deref(), Some("/support"));
173 assert!(tool.autosubmit);
174
175 let expected_schema = json!({
177 "type": "object",
178 "properties": {
179 "subject": { "type": "string", "description": "Short summary" },
180 "urgent": { "type": "boolean" },
181 "category": { "type": "string", "enum": ["billing", "tech"] }
182 },
183 "required": ["subject"]
184 });
185 assert_eq!(tool.input_schema, expected_schema);
186
187 assert!(tool.extra.contains_key("integrity"));
190 assert!(tool.extra.contains_key("source"));
191 assert_eq!(tool.extra["source"], json!("declarative"));
192 assert_eq!(
193 tool.extra["integrity"],
194 json!({ "trusted": false, "confidence": 0.7,
195 "hints": { "readOnly": false, "untrustedContent": true } })
196 );
197
198 let wire: serde_json::Value = serde_json::from_str(SAMPLE).expect("sample is valid json");
201 let reserialized = serde_json::to_value(tool).expect("tool must serialize");
202 assert_eq!(reserialized, wire["tools"][0]);
203 }
204
205 #[test]
206 fn action_absent_deserializes_to_none() {
207 let returns: ListToolsReturns = serde_json::from_value(json!({
208 "tools": [{
209 "name": "noAction",
210 "description": "imperative tool",
211 "inputSchema": { "type": "object" },
212 "autosubmit": false
213 }]
214 }))
215 .expect("tool without action must deserialize");
216 let tool = &returns.tools[0];
217 assert_eq!(tool.action, None);
218 let reserialized = serde_json::to_value(tool).expect("tool must serialize");
220 assert!(reserialized.get("action").is_none());
221 }
222
223 #[test]
224 fn method_identifiers_match_the_domain() {
225 use chromiumoxide_types::Method;
226
227 assert_eq!(ListToolsParams::IDENTIFIER, "WebMCP.listTools");
228 assert_eq!(CallToolParams::IDENTIFIER, "WebMCP.callTool");
229 assert_eq!(ListToolsParams::default().identifier(), "WebMCP.listTools");
230 assert_eq!(
231 CallToolParams::new("t", json!({})).identifier(),
232 "WebMCP.callTool"
233 );
234 assert_eq!(
236 serde_json::to_value(ListToolsParams::default()).expect("params must serialize"),
237 json!({})
238 );
239 }
240
241 #[test]
242 fn tolerates_unknown_fields() {
243 let returns: ListToolsReturns = serde_json::from_value(json!({
244 "tools": [{
245 "name": "future",
246 "description": "from a newer server phase",
247 "inputSchema": {},
248 "futureField": 1
249 }],
250 "futureTopLevel": true
251 }))
252 .expect("unknown fields must be tolerated");
253 let tool = &returns.tools[0];
254 assert_eq!(tool.name, "future");
255 assert_eq!(tool.extra["futureField"], json!(1));
256 }
257}