use std::{borrow::Cow, sync::Arc};
#[cfg(feature = "server")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{Icon, JsonObject, Meta};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct Tool {
pub name: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
pub input_schema: Arc<JsonObject>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_schema: Option<Arc<JsonObject>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution: Option<ToolExecution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
pub enum TaskSupport {
#[default]
Forbidden,
Optional,
Required,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ToolExecution {
#[serde(skip_serializing_if = "Option::is_none")]
pub task_support: Option<TaskSupport>,
}
impl ToolExecution {
pub fn new() -> Self {
Self::default()
}
pub fn from_raw(task_support: Option<TaskSupport>) -> Self {
Self { task_support }
}
pub fn with_task_support(mut self, task_support: TaskSupport) -> Self {
self.task_support = Some(task_support);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
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>,
}
impl ToolAnnotations {
pub fn new() -> Self {
Self::default()
}
pub fn from_raw(
title: Option<String>,
read_only_hint: Option<bool>,
destructive_hint: Option<bool>,
idempotent_hint: Option<bool>,
open_world_hint: Option<bool>,
) -> Self {
ToolAnnotations {
title,
read_only_hint,
destructive_hint,
idempotent_hint,
open_world_hint,
}
}
pub fn with_title<T>(title: T) -> Self
where
T: Into<String>,
{
ToolAnnotations {
title: Some(title.into()),
..Self::default()
}
}
pub fn read_only(self, read_only: bool) -> Self {
ToolAnnotations {
read_only_hint: Some(read_only),
..self
}
}
pub fn destructive(self, destructive: bool) -> Self {
ToolAnnotations {
destructive_hint: Some(destructive),
..self
}
}
pub fn idempotent(self, idempotent: bool) -> Self {
ToolAnnotations {
idempotent_hint: Some(idempotent),
..self
}
}
pub fn open_world(self, open_world: bool) -> Self {
ToolAnnotations {
open_world_hint: Some(open_world),
..self
}
}
pub fn is_destructive(&self) -> bool {
self.destructive_hint.unwrap_or(true)
}
pub fn is_idempotent(&self) -> bool {
self.idempotent_hint.unwrap_or(false)
}
}
impl Tool {
pub fn new<N, D, S>(name: N, description: D, input_schema: S) -> Self
where
N: Into<Cow<'static, str>>,
D: Into<Cow<'static, str>>,
S: Into<Arc<JsonObject>>,
{
Tool {
name: name.into(),
title: None,
description: Some(description.into()),
input_schema: input_schema.into(),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
}
}
pub fn new_with_raw<N, S>(
name: N,
description: Option<Cow<'static, str>>,
input_schema: S,
) -> Self
where
N: Into<Cow<'static, str>>,
S: Into<Arc<JsonObject>>,
{
Tool {
name: name.into(),
title: None,
description,
input_schema: input_schema.into(),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_raw_output_schema(mut self, output_schema: Arc<JsonObject>) -> Self {
self.output_schema = Some(output_schema);
self
}
pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
self.annotations = Some(annotations);
self
}
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
pub fn annotate(self, annotations: ToolAnnotations) -> Self {
Tool {
annotations: Some(annotations),
..self
}
}
pub fn with_execution(mut self, execution: ToolExecution) -> Self {
self.execution = Some(execution);
self
}
pub fn task_support(&self) -> TaskSupport {
self.execution
.as_ref()
.and_then(|e| e.task_support)
.unwrap_or_default()
}
#[cfg(feature = "server")]
pub fn with_output_schema<T: JsonSchema + 'static>(mut self) -> Self {
let schema = crate::handler::server::tool::schema_for_output::<T>()
.unwrap_or_else(|e| panic!("Invalid output schema for tool '{}': {}", self.name, e));
self.output_schema = Some(schema);
self
}
#[cfg(feature = "server")]
pub fn with_input_schema<T: JsonSchema + 'static>(mut self) -> Self {
self.input_schema = crate::handler::server::tool::schema_for_type::<T>();
self
}
pub fn schema_as_json_value(&self) -> Value {
Value::Object(self.input_schema.as_ref().clone())
}
}