use std::{collections::HashMap, mem, result::Result as StdResult};
use schemars::generate::SchemaSettings;
use serde::{
Deserialize, Serialize,
de::{DeserializeOwned, Error as DeError},
};
use serde_json::{Value, json};
use super::*;
use crate::macros::{with_basename, with_meta};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListToolsResult {
pub tools: Vec<Tool>,
#[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<Cursor>,
}
impl ListToolsResult {
pub fn new() -> Self {
Self {
tools: Vec::new(),
next_cursor: None,
}
}
pub fn with_tool(mut self, tool: Tool) -> Self {
self.tools.push(tool);
self
}
pub fn with_tools(mut self, tools: impl IntoIterator<Item = Tool>) -> Self {
self.tools.extend(tools);
self
}
pub fn with_cursor(mut self, cursor: impl Into<Cursor>) -> Self {
self.next_cursor = Some(cursor.into());
self
}
}
#[with_meta]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolResult {
pub content: Vec<ContentBlock>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
pub structured_content: Option<Value>,
}
impl CallToolResult {
pub fn new() -> Self {
Self {
content: Vec::new(),
is_error: None,
structured_content: None,
_meta: None,
}
}
pub fn structured(content: impl Serialize) -> StdResult<Self, serde_json::Error> {
Ok(Self::new().with_structured_content(serde_json::to_value(content)?))
}
pub fn error(code: &'static str, message: impl Into<String>) -> Self {
Self::new().mark_as_error().with_structured_content(json!({
"code": code,
"message": message.into(),
}))
}
pub fn with_content(mut self, content: ContentBlock) -> Self {
self.content.push(content);
self
}
pub fn with_text_content(mut self, text: impl Into<String>) -> Self {
self.content.push(ContentBlock::Text(TextContent {
text: text.into(),
annotations: None,
_meta: None,
}));
self
}
pub fn with_json_text(self, data: impl Serialize) -> StdResult<Self, serde_json::Error> {
let text = serde_json::to_string(&data)?;
Ok(self.with_text_content(text))
}
pub fn mark_as_error(mut self) -> Self {
self.is_error = Some(true);
self
}
pub fn with_structured_content(mut self, content: Value) -> Self {
self.structured_content = Some(content);
self
}
pub fn text(&self) -> Option<&str> {
self.content.iter().find_map(|block| match block {
ContentBlock::Text(text) => Some(text.text.as_str()),
_ => None,
})
}
pub fn all_text(&self) -> String {
self.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn json<T: DeserializeOwned>(&self) -> StdResult<T, serde_json::Error> {
let text = self
.text()
.ok_or_else(|| DeError::custom("no text content in tool result"))?;
serde_json::from_str(text)
}
pub fn structured_as<T: DeserializeOwned>(&self) -> StdResult<T, serde_json::Error> {
let value = self
.structured_content
.as_ref()
.ok_or_else(|| DeError::custom("no structured content in tool result"))?;
serde_json::from_value(value.clone())
}
}
pub trait ToolResponse {
fn into_call_tool_result(self) -> CallToolResult;
}
impl<T: ToolResponse> From<T> for CallToolResult {
fn from(value: T) -> Self {
value.into_call_tool_result()
}
}
impl ToolResponse for Value {
fn into_call_tool_result(self) -> CallToolResult {
CallToolResult::new().with_structured_content(self)
}
}
impl Default for CallToolResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecution {
#[serde(rename = "taskSupport", skip_serializing_if = "Option::is_none")]
pub task_support: Option<ToolTaskSupport>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolTaskSupport {
Forbidden,
Optional,
Required,
}
#[with_meta]
#[with_basename]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
#[serde(rename = "inputSchema")]
pub input_schema: ToolSchema,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution: Option<ToolExecution>,
#[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
pub output_schema: Option<ToolSchema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
}
impl Tool {
pub fn new(name: impl Into<String>, input_schema: ToolSchema) -> Self {
Self {
input_schema,
description: None,
execution: None,
output_schema: None,
annotations: None,
icons: None,
name: name.into(),
title: None,
_meta: None,
}
}
pub fn from_schema<T: schemars::JsonSchema>(name: impl Into<String>) -> Self {
let schema = ToolSchema::from_json_schema::<T>();
let description = schema.description().map(|s| s.to_string());
let title = schema.title().map(|s| s.to_string());
let mut tool = Self::new(name, schema);
if let Some(desc) = description {
tool.description = Some(desc);
}
if let Some(t) = title {
tool.title = Some(t);
}
tool
}
pub fn with_schema<T: schemars::JsonSchema>(mut self) -> Self {
self.input_schema = ToolSchema::from_json_schema::<T>();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_output_schema(mut self, schema: ToolSchema) -> Self {
self.output_schema = Some(schema);
self
}
pub fn with_execution(mut self, execution: ToolExecution) -> Self {
self.execution = Some(execution);
self
}
pub fn with_task_support(mut self, support: ToolTaskSupport) -> Self {
self.execution
.get_or_insert(ToolExecution { task_support: None })
.task_support = Some(support);
self
}
pub fn with_annotation_title(mut self, title: impl Into<String>) -> Self {
self.annotations.get_or_insert_with(Default::default).title = Some(title.into());
self
}
pub fn with_read_only_hint(mut self, read_only: bool) -> Self {
self.annotations
.get_or_insert_with(Default::default)
.read_only_hint = Some(read_only);
self
}
pub fn with_destructive_hint(mut self, destructive: bool) -> Self {
self.annotations
.get_or_insert_with(Default::default)
.destructive_hint = Some(destructive);
self
}
pub fn with_idempotent_hint(mut self, idempotent: bool) -> Self {
self.annotations
.get_or_insert_with(Default::default)
.idempotent_hint = Some(idempotent);
self
}
pub fn with_open_world_hint(mut self, open_world: bool) -> Self {
self.annotations
.get_or_insert_with(Default::default)
.open_world_hint = Some(open_world);
self
}
pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
self.annotations = Some(annotations);
self
}
pub fn with_icons(mut self, icons: impl IntoIterator<Item = Icon>) -> Self {
self.icons = Some(icons.into_iter().collect());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ToolSchema(pub Value);
impl Default for ToolSchema {
fn default() -> Self {
Self::new(serde_json::json!({
"type": "object"
}))
}
}
impl ToolSchema {
pub fn new(schema: Value) -> Self {
Self(schema)
}
pub fn empty() -> Self {
Self::default()
}
pub fn description(&self) -> Option<&str> {
self.0.get("description").and_then(|v| v.as_str())
}
pub fn title(&self) -> Option<&str> {
self.0.get("title").and_then(|v| v.as_str())
}
pub fn as_value(&self) -> &Value {
&self.0
}
pub fn as_value_mut(&mut self) -> &mut Value {
&mut self.0
}
pub fn schema_type(&self) -> Option<&str> {
self.0.get("type").and_then(|v| v.as_str())
}
pub fn properties(&self) -> Option<&serde_json::Map<String, Value>> {
self.0.get("properties").and_then(|v| v.as_object())
}
pub fn required(&self) -> Option<Vec<&str>> {
self.0
.get("required")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
}
pub fn with_property(mut self, name: impl Into<String>, schema: Value) -> Self {
if let Some(obj) = self.0.as_object_mut() {
let properties = obj
.entry("properties")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
if let Some(props) = properties.as_object_mut() {
props.insert(name.into(), schema);
}
}
self
}
pub fn with_properties(mut self, properties: HashMap<String, Value>) -> Self {
if let Some(obj) = self.0.as_object_mut() {
obj.insert(
"properties".to_string(),
Value::Object(properties.into_iter().collect()),
);
}
self
}
pub fn with_required(mut self, name: impl Into<String>) -> Self {
if let Some(obj) = self.0.as_object_mut() {
let required = obj
.entry("required")
.or_insert_with(|| Value::Array(Vec::new()));
if let Some(arr) = required.as_array_mut() {
arr.push(Value::String(name.into()));
}
}
self
}
pub fn with_required_properties(
mut self,
names: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
if let Some(obj) = self.0.as_object_mut() {
let required = obj
.entry("required")
.or_insert_with(|| Value::Array(Vec::new()));
if let Some(arr) = required.as_array_mut() {
arr.extend(names.into_iter().map(|n| Value::String(n.into())));
}
}
self
}
pub fn from_json_schema<T: schemars::JsonSchema>() -> Self {
let mut settings = SchemaSettings::draft2020_12();
settings.inline_subschemas = true;
settings.meta_schema = None;
let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<T>();
let mut value =
serde_json::to_value(&schema).unwrap_or_else(|_| Value::Object(Default::default()));
simplify_schema_value(&mut value);
force_object_schema(&mut value);
Self(value)
}
pub fn is_required(&self, name: &str) -> bool {
self.required()
.map(|req| req.contains(&name))
.unwrap_or(false)
}
}
fn simplify_schema_value(value: &mut Value) {
match value {
Value::Object(map) => {
if let Some(replacement) = simplify_nullable_anyof(map) {
*value = replacement;
simplify_schema_value(value);
return;
}
simplify_oneof_enum(map);
for v in map.values_mut() {
simplify_schema_value(v);
}
}
Value::Array(items) => {
for item in items {
simplify_schema_value(item);
}
}
_ => {}
}
}
fn simplify_nullable_anyof(map: &serde_json::Map<String, Value>) -> Option<Value> {
let any_of = map.get("anyOf")?.as_array()?;
if any_of.len() != 2 {
return None;
}
let (_null_idx, other_idx) = if is_null_schema(&any_of[0]) {
(0, 1)
} else if is_null_schema(&any_of[1]) {
(1, 0)
} else {
return None;
};
let mut replacement = any_of[other_idx].clone();
if let Value::Object(obj) = &mut replacement {
for key in ["description", "title", "default", "examples"] {
if let Some(value) = map.get(key) {
obj.entry(key.to_string()).or_insert_with(|| value.clone());
}
}
}
add_null_type(&mut replacement);
if let Value::Object(obj) = &mut replacement {
obj.remove("anyOf");
}
Some(replacement)
}
fn simplify_oneof_enum(map: &mut serde_json::Map<String, Value>) {
let Some(Value::Array(one_of)) = map.get("oneOf") else {
return;
};
if one_of.is_empty() || !one_of.iter().all(is_const_variant) {
return;
}
let mut enums = Vec::with_capacity(one_of.len());
let mut types = Vec::new();
for entry in one_of {
let Value::Object(obj) = entry else { continue };
if let Some(const_val) = obj.get("const") {
enums.push(const_val.clone());
}
if let Some(Value::String(ty)) = obj.get("type")
&& !types.iter().any(|t| t == ty)
{
types.push(ty.clone());
}
}
if enums.is_empty() {
return;
}
map.remove("oneOf");
map.insert("enum".to_string(), Value::Array(enums));
if !types.is_empty() {
match map.get_mut("type") {
Some(Value::String(existing)) => {
let mut values = Vec::with_capacity(types.len() + 1);
values.push(Value::String(mem::take(existing)));
for ty in types {
if !values
.iter()
.any(|v| matches!(v, Value::String(s) if s == &ty))
{
values.push(Value::String(ty));
}
}
*map.get_mut("type").unwrap() = Value::Array(values);
}
Some(Value::Array(existing)) => {
for ty in types {
if !existing
.iter()
.any(|v| matches!(v, Value::String(s) if s == &ty))
{
existing.push(Value::String(ty));
}
}
}
Some(_) => {}
None => {
if types.len() == 1 {
map.insert("type".to_string(), Value::String(types.remove(0)));
} else {
map.insert(
"type".to_string(),
Value::Array(types.into_iter().map(Value::String).collect()),
);
}
}
}
}
}
fn is_const_variant(value: &Value) -> bool {
let Value::Object(obj) = value else {
return false;
};
if !obj.contains_key("const") {
return false;
}
obj.keys().all(|key| {
matches!(
key.as_str(),
"const" | "description" | "type" | "title" | "deprecated"
)
})
}
fn is_null_schema(value: &Value) -> bool {
match value {
Value::Object(obj) => {
if let Some(Value::String(ty)) = obj.get("type") {
ty == "null"
} else if let Some(Value::Array(types)) = obj.get("type") {
types
.iter()
.any(|t| matches!(t, Value::String(s) if s == "null"))
} else if let Some(Value::Null) = obj.get("const") {
true
} else if let Some(Value::Array(values)) = obj.get("enum") {
values.iter().any(|v| v.is_null())
} else {
false
}
}
_ => false,
}
}
fn add_null_type(value: &mut Value) {
let Value::Object(obj) = value else {
return;
};
if let Some(ty) = obj.get_mut("type") {
match ty {
Value::String(s) if s != "null" => {
let current = mem::take(s);
*ty = Value::Array(vec![Value::String(current), Value::String("null".into())]);
}
Value::Array(arr)
if !arr
.iter()
.any(|v| matches!(v, Value::String(s) if s == "null")) =>
{
arr.push(Value::String("null".into()));
}
_ => {}
}
} else {
obj.insert(
"type".to_string(),
Value::Array(vec![Value::String("null".into())]),
);
}
}
fn force_object_schema(value: &mut Value) {
let Value::Object(map) = value else {
return;
};
let has_object_shape = map.contains_key("properties")
|| map.contains_key("required")
|| map.contains_key("additionalProperties");
match map.get_mut("type") {
Some(Value::String(ty)) => {
if ty != "object" && has_object_shape {
*ty = "object".to_string();
}
}
Some(Value::Array(types)) => {
let has_object = types
.iter()
.any(|v| matches!(v, Value::String(s) if s == "object"));
if has_object || has_object_shape {
map.insert("type".to_string(), Value::String("object".to_string()));
}
}
Some(_) => {
if has_object_shape {
map.insert("type".to_string(), Value::String("object".to_string()));
}
}
None => {
if has_object_shape {
map.insert("type".to_string(), Value::String("object".to_string()));
}
}
}
if matches!(map.get("type"), Some(Value::String(s)) if s == "object") {
map.entry("properties")
.or_insert_with(|| Value::Object(Default::default()));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect_refs(value: &Value, refs: &mut Vec<String>) {
match value {
Value::Object(map) => {
if let Some(Value::String(reference)) = map.get("$ref") {
refs.push(reference.clone());
}
for entry in map.values() {
collect_refs(entry, refs);
}
}
Value::Array(items) => {
for entry in items {
collect_refs(entry, refs);
}
}
_ => {}
}
}
fn ref_target_exists(schema: &Value, reference: &str) -> bool {
let Some(pointer) = reference.strip_prefix('#') else {
return true;
};
if pointer.is_empty() {
return true;
}
schema.pointer(pointer).is_some()
}
#[test]
fn test_call_tool_result_text() {
let result = CallToolResult::new();
assert!(result.text().is_none());
let result = CallToolResult::new().with_text_content("hello world");
assert_eq!(result.text(), Some("hello world"));
let result = CallToolResult::new()
.with_text_content("first")
.with_text_content("second");
assert_eq!(result.text(), Some("first"));
}
#[test]
fn test_call_tool_result_all_text() {
let result = CallToolResult::new();
assert_eq!(result.all_text(), "");
let result = CallToolResult::new().with_text_content("hello world");
assert_eq!(result.all_text(), "hello world");
let result = CallToolResult::new()
.with_text_content("line one")
.with_text_content("line two")
.with_text_content("line three");
assert_eq!(result.all_text(), "line one\nline two\nline three");
}
#[test]
fn test_call_tool_result_json() {
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
struct Response {
value: i32,
message: String,
}
let data = Response {
value: 42,
message: "hello".to_string(),
};
let result = CallToolResult::new().with_json_text(&data).unwrap();
assert_eq!(result.text(), Some(r#"{"value":42,"message":"hello"}"#));
let result =
CallToolResult::new().with_text_content(r#"{"value": 42, "message": "hello"}"#);
let parsed: Response = result.json().unwrap();
assert_eq!(parsed.value, 42);
assert_eq!(parsed.message, "hello");
let result = CallToolResult::new();
let err = result.json::<Response>().unwrap_err();
assert!(err.to_string().contains("no text content"));
let result = CallToolResult::new().with_text_content("not json");
assert!(result.json::<Response>().is_err());
}
#[test]
fn test_call_tool_result_structured() {
#[derive(serde::Serialize)]
struct Payload {
value: i32,
}
let result = CallToolResult::structured(Payload { value: 42 }).unwrap();
assert_eq!(result.structured_content, Some(json!({ "value": 42 })));
assert_eq!(result.is_error, None);
}
#[test]
fn test_call_tool_result_error() {
let result = CallToolResult::error("BAD_INPUT", "oops");
assert_eq!(result.is_error, Some(true));
assert_eq!(
result.structured_content,
Some(json!({ "code": "BAD_INPUT", "message": "oops" }))
);
}
#[test]
fn test_tool_from_schema() {
use schemars::JsonSchema;
#[allow(dead_code)] #[derive(JsonSchema)]
struct EchoParams {
message: String,
count: Option<i32>,
}
let tool = Tool::from_schema::<EchoParams>("echo");
assert_eq!(tool.name, "echo");
let props = tool.input_schema.properties().unwrap();
assert!(props.contains_key("message"));
assert!(props.contains_key("count"));
}
#[test]
fn test_tool_schema_keeps_ref_definitions() {
use schemars::JsonSchema;
#[derive(JsonSchema)]
struct Node {
value: i32,
next: Option<Box<Self>>,
}
let schema = ToolSchema::from_json_schema::<Node>();
let value = schema.as_value();
let mut refs = Vec::new();
collect_refs(value, &mut refs);
assert!(!refs.is_empty(), "expected schema to contain $ref entries");
for reference in refs {
assert!(
ref_target_exists(value, &reference),
"missing schema definition for {reference}"
);
}
let node = Node {
value: 1,
next: None,
};
let _ = node.value;
let _ = node.next;
}
#[test]
fn test_tool_with_schema() {
use schemars::JsonSchema;
#[allow(dead_code)] #[derive(JsonSchema)]
struct MyParams {
name: String,
}
let tool = Tool::new("test", ToolSchema::empty())
.with_description("A test tool")
.with_schema::<MyParams>();
assert_eq!(tool.name, "test");
assert_eq!(tool.description.as_deref(), Some("A test tool"));
let props = tool.input_schema.properties().unwrap();
assert!(props.contains_key("name"));
}
#[test]
fn test_tool_with_title() {
let tool = Tool::new("test", ToolSchema::empty())
.with_title("Test Tool")
.with_description("A test tool");
assert_eq!(tool.title.as_deref(), Some("Test Tool"));
assert_eq!(tool.description.as_deref(), Some("A test tool"));
}
#[test]
fn test_tool_schema_empty() {
let schema = ToolSchema::empty();
assert_eq!(schema.schema_type(), Some("object"));
assert!(schema.properties().is_none());
assert!(schema.as_value().get("$schema").is_none());
}
#[test]
fn test_tool_schema_description_and_title() {
let schema = ToolSchema::new(serde_json::json!({
"type": "object",
"title": "My Schema",
"description": "A schema for testing"
}));
assert_eq!(schema.title(), Some("My Schema"));
assert_eq!(schema.description(), Some("A schema for testing"));
let empty = ToolSchema::empty();
assert!(empty.title().is_none());
assert!(empty.description().is_none());
}
#[test]
fn test_tool_schema_from_json_schema_no_dialect() {
use schemars::JsonSchema;
#[derive(JsonSchema)]
struct Params {
name: String,
}
let params = Params {
name: String::new(),
};
let _ = params.name;
let schema = ToolSchema::from_json_schema::<Params>();
assert!(schema.as_value().get("$schema").is_none());
}
}