use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema", default)]
pub input_schema: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(default)]
pub autosubmit: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ListToolsParams {}
impl ListToolsParams {
pub const IDENTIFIER: &'static str = "WebMCP.listTools";
}
impl chromiumoxide_types::Method for ListToolsParams {
fn identifier(&self) -> chromiumoxide_types::MethodId {
Self::IDENTIFIER.into()
}
}
impl chromiumoxide_types::MethodType for ListToolsParams {
fn method_id() -> chromiumoxide_types::MethodId
where
Self: Sized,
{
Self::IDENTIFIER.into()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ListToolsReturns {
#[serde(default)]
pub tools: Vec<Tool>,
}
impl chromiumoxide_types::Command for ListToolsParams {
type Response = ListToolsReturns;
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(default)]
pub arguments: serde_json::Value,
}
impl CallToolParams {
pub const IDENTIFIER: &'static str = "WebMCP.callTool";
pub fn new(name: impl Into<String>, arguments: serde_json::Value) -> Self {
Self {
name: name.into(),
arguments,
}
}
}
impl chromiumoxide_types::Method for CallToolParams {
fn identifier(&self) -> chromiumoxide_types::MethodId {
Self::IDENTIFIER.into()
}
}
impl chromiumoxide_types::MethodType for CallToolParams {
fn method_id() -> chromiumoxide_types::MethodId
where
Self: Sized,
{
Self::IDENTIFIER.into()
}
}
impl chromiumoxide_types::Command for CallToolParams {
type Response = serde_json::Value;
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const SAMPLE: &str = r#"{ "tools": [ {
"name": "createSupportRequest",
"description": "File a support ticket",
"inputSchema": { "type": "object",
"properties": { "subject": { "type": "string", "description": "Short summary" },
"urgent": { "type": "boolean" },
"category": { "type": "string", "enum": ["billing","tech"] } },
"required": ["subject"] },
"source": "declarative",
"action": "/support",
"autosubmit": true,
"integrity": { "trusted": false, "confidence": 0.7,
"hints": { "readOnly": false, "untrustedContent": true } }
} ] }"#;
#[test]
fn deserializes_list_tools_sample_payload() {
let returns: ListToolsReturns =
serde_json::from_str(SAMPLE).expect("sample must deserialize");
assert_eq!(returns.tools.len(), 1);
let tool = &returns.tools[0];
assert_eq!(tool.name, "createSupportRequest");
assert_eq!(tool.description.as_deref(), Some("File a support ticket"));
assert_eq!(tool.action.as_deref(), Some("/support"));
assert!(tool.autosubmit);
let expected_schema = json!({
"type": "object",
"properties": {
"subject": { "type": "string", "description": "Short summary" },
"urgent": { "type": "boolean" },
"category": { "type": "string", "enum": ["billing", "tech"] }
},
"required": ["subject"]
});
assert_eq!(tool.input_schema, expected_schema);
assert!(tool.extra.contains_key("integrity"));
assert!(tool.extra.contains_key("source"));
assert_eq!(tool.extra["source"], json!("declarative"));
assert_eq!(
tool.extra["integrity"],
json!({ "trusted": false, "confidence": 0.7,
"hints": { "readOnly": false, "untrustedContent": true } })
);
let wire: serde_json::Value = serde_json::from_str(SAMPLE).expect("sample is valid json");
let reserialized = serde_json::to_value(tool).expect("tool must serialize");
assert_eq!(reserialized, wire["tools"][0]);
}
#[test]
fn action_absent_deserializes_to_none() {
let returns: ListToolsReturns = serde_json::from_value(json!({
"tools": [{
"name": "noAction",
"description": "imperative tool",
"inputSchema": { "type": "object" },
"autosubmit": false
}]
}))
.expect("tool without action must deserialize");
let tool = &returns.tools[0];
assert_eq!(tool.action, None);
let reserialized = serde_json::to_value(tool).expect("tool must serialize");
assert!(reserialized.get("action").is_none());
}
#[test]
fn method_identifiers_match_the_domain() {
use chromiumoxide_types::Method;
assert_eq!(ListToolsParams::IDENTIFIER, "WebMCP.listTools");
assert_eq!(CallToolParams::IDENTIFIER, "WebMCP.callTool");
assert_eq!(ListToolsParams::default().identifier(), "WebMCP.listTools");
assert_eq!(
CallToolParams::new("t", json!({})).identifier(),
"WebMCP.callTool"
);
assert_eq!(
serde_json::to_value(ListToolsParams::default()).expect("params must serialize"),
json!({})
);
}
#[test]
fn tolerates_unknown_fields() {
let returns: ListToolsReturns = serde_json::from_value(json!({
"tools": [{
"name": "future",
"description": "from a newer server phase",
"inputSchema": {},
"futureField": 1
}],
"futureTopLevel": true
}))
.expect("unknown fields must be tolerated");
let tool = &returns.tools[0];
assert_eq!(tool.name, "future");
assert_eq!(tool.extra["futureField"], json!(1));
}
}