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)]
98pub struct CallToolParams {
99 pub name: String,
101 #[serde(default)]
103 pub arguments: serde_json::Value,
104}
105
106impl CallToolParams {
107 pub const IDENTIFIER: &'static str = "WebMCP.callTool";
108
109 pub fn new(name: impl Into<String>, arguments: serde_json::Value) -> Self {
111 Self {
112 name: name.into(),
113 arguments,
114 }
115 }
116}
117
118impl chromiumoxide_types::Method for CallToolParams {
119 fn identifier(&self) -> chromiumoxide_types::MethodId {
120 Self::IDENTIFIER.into()
121 }
122}
123
124impl chromiumoxide_types::MethodType for CallToolParams {
125 fn method_id() -> chromiumoxide_types::MethodId
126 where
127 Self: Sized,
128 {
129 Self::IDENTIFIER.into()
130 }
131}
132
133impl chromiumoxide_types::Command for CallToolParams {
134 type Response = serde_json::Value;
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use serde_json::json;
141
142 const SAMPLE: &str = r#"{ "tools": [ {
143 "name": "createSupportRequest",
144 "description": "File a support ticket",
145 "inputSchema": { "type": "object",
146 "properties": { "subject": { "type": "string", "description": "Short summary" },
147 "urgent": { "type": "boolean" },
148 "category": { "type": "string", "enum": ["billing","tech"] } },
149 "required": ["subject"] },
150 "source": "declarative",
151 "action": "/support",
152 "autosubmit": true,
153 "integrity": { "trusted": false, "confidence": 0.7,
154 "hints": { "readOnly": false, "untrustedContent": true } }
155 } ] }"#;
156
157 #[test]
158 fn deserializes_list_tools_sample_payload() {
159 let returns: ListToolsReturns =
160 serde_json::from_str(SAMPLE).expect("sample must deserialize");
161 assert_eq!(returns.tools.len(), 1);
162 let tool = &returns.tools[0];
163
164 assert_eq!(tool.name, "createSupportRequest");
166 assert_eq!(tool.description.as_deref(), Some("File a support ticket"));
167 assert_eq!(tool.action.as_deref(), Some("/support"));
168 assert!(tool.autosubmit);
169
170 let expected_schema = json!({
172 "type": "object",
173 "properties": {
174 "subject": { "type": "string", "description": "Short summary" },
175 "urgent": { "type": "boolean" },
176 "category": { "type": "string", "enum": ["billing", "tech"] }
177 },
178 "required": ["subject"]
179 });
180 assert_eq!(tool.input_schema, expected_schema);
181
182 assert!(tool.extra.contains_key("integrity"));
185 assert!(tool.extra.contains_key("source"));
186 assert_eq!(tool.extra["source"], json!("declarative"));
187 assert_eq!(
188 tool.extra["integrity"],
189 json!({ "trusted": false, "confidence": 0.7,
190 "hints": { "readOnly": false, "untrustedContent": true } })
191 );
192
193 let wire: serde_json::Value = serde_json::from_str(SAMPLE).expect("sample is valid json");
196 let reserialized = serde_json::to_value(tool).expect("tool must serialize");
197 assert_eq!(reserialized, wire["tools"][0]);
198 }
199
200 #[test]
201 fn action_absent_deserializes_to_none() {
202 let returns: ListToolsReturns = serde_json::from_value(json!({
203 "tools": [{
204 "name": "noAction",
205 "description": "imperative tool",
206 "inputSchema": { "type": "object" },
207 "autosubmit": false
208 }]
209 }))
210 .expect("tool without action must deserialize");
211 let tool = &returns.tools[0];
212 assert_eq!(tool.action, None);
213 let reserialized = serde_json::to_value(tool).expect("tool must serialize");
215 assert!(reserialized.get("action").is_none());
216 }
217
218 #[test]
219 fn method_identifiers_match_the_domain() {
220 use chromiumoxide_types::Method;
221
222 assert_eq!(ListToolsParams::IDENTIFIER, "WebMCP.listTools");
223 assert_eq!(CallToolParams::IDENTIFIER, "WebMCP.callTool");
224 assert_eq!(ListToolsParams::default().identifier(), "WebMCP.listTools");
225 assert_eq!(
226 CallToolParams::new("t", json!({})).identifier(),
227 "WebMCP.callTool"
228 );
229 assert_eq!(
231 serde_json::to_value(ListToolsParams::default()).expect("params must serialize"),
232 json!({})
233 );
234 }
235
236 #[test]
237 fn tolerates_unknown_fields() {
238 let returns: ListToolsReturns = serde_json::from_value(json!({
239 "tools": [{
240 "name": "future",
241 "description": "from a newer server phase",
242 "inputSchema": {},
243 "futureField": 1
244 }],
245 "futureTopLevel": true
246 }))
247 .expect("unknown fields must be tolerated");
248 let tool = &returns.tools[0];
249 assert_eq!(tool.name, "future");
250 assert_eq!(tool.extra["futureField"], json!(1));
251 }
252}