Skip to main content

rmcp/model/
tool.rs

1use std::{borrow::Cow, sync::Arc};
2
3#[cfg(feature = "server")]
4use schemars::JsonSchema;
5/// Tools represent a routine that a server can execute
6/// Tool calls represent requests from the client to execute one
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use super::{Icon, JsonObject, MetaObject};
11
12/// A tool that can be used by a model.
13#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
16#[non_exhaustive]
17pub struct Tool {
18    /// The name of the tool
19    pub name: Cow<'static, str>,
20    /// A human-readable title for the tool
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub title: Option<String>,
23    /// A description of what the tool does
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub description: Option<Cow<'static, str>>,
26    /// A JSON Schema object defining the expected parameters for the tool
27    pub input_schema: Arc<JsonObject>,
28    /// An optional JSON Schema object defining the structure of the tool's output
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub output_schema: Option<Arc<JsonObject>>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    /// Optional additional tool information.
33    pub annotations: Option<ToolAnnotations>,
34    /// Optional list of icons for the tool
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub icons: Option<Vec<Icon>>,
37    /// Optional additional metadata for this tool
38    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
39    pub meta: Option<MetaObject>,
40}
41
42/// Additional properties describing a Tool to clients.
43///
44/// NOTE: all properties in ToolAnnotations are **hints**.
45/// They are not guaranteed to provide a faithful description of
46/// tool behavior (including descriptive properties like `title`).
47///
48/// Clients should never make tool use decisions based on ToolAnnotations
49/// received from untrusted servers.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
51#[serde(rename_all = "camelCase")]
52#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
53#[non_exhaustive]
54pub struct ToolAnnotations {
55    /// A human-readable title for the tool.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub title: Option<String>,
58
59    /// If true, the tool does not modify its environment.
60    ///
61    /// Default: false
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub read_only_hint: Option<bool>,
64
65    /// If true, the tool may perform destructive updates to its environment.
66    /// If false, the tool performs only additive updates.
67    ///
68    /// (This property is meaningful only when `readOnlyHint == false`)
69    ///
70    /// Default: true
71    /// A human-readable description of the tool's purpose.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub destructive_hint: Option<bool>,
74
75    /// If true, calling the tool repeatedly with the same arguments
76    /// will have no additional effect on the its environment.
77    ///
78    /// (This property is meaningful only when `readOnlyHint == false`)
79    ///
80    /// Default: false.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub idempotent_hint: Option<bool>,
83
84    /// If true, this tool may interact with an "open world" of external
85    /// entities. If false, the tool's domain of interaction is closed.
86    /// For example, the world of a web search tool is open, whereas that
87    /// of a memory tool is not.
88    ///
89    /// Default: true
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub open_world_hint: Option<bool>,
92}
93
94impl ToolAnnotations {
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Create a new ToolAnnotations with all fields specified
100    pub fn from_raw(
101        title: Option<String>,
102        read_only_hint: Option<bool>,
103        destructive_hint: Option<bool>,
104        idempotent_hint: Option<bool>,
105        open_world_hint: Option<bool>,
106    ) -> Self {
107        ToolAnnotations {
108            title,
109            read_only_hint,
110            destructive_hint,
111            idempotent_hint,
112            open_world_hint,
113        }
114    }
115
116    pub fn with_title<T>(title: T) -> Self
117    where
118        T: Into<String>,
119    {
120        ToolAnnotations {
121            title: Some(title.into()),
122            ..Self::default()
123        }
124    }
125    pub fn read_only(self, read_only: bool) -> Self {
126        ToolAnnotations {
127            read_only_hint: Some(read_only),
128            ..self
129        }
130    }
131    pub fn destructive(self, destructive: bool) -> Self {
132        ToolAnnotations {
133            destructive_hint: Some(destructive),
134            ..self
135        }
136    }
137    pub fn idempotent(self, idempotent: bool) -> Self {
138        ToolAnnotations {
139            idempotent_hint: Some(idempotent),
140            ..self
141        }
142    }
143    pub fn open_world(self, open_world: bool) -> Self {
144        ToolAnnotations {
145            open_world_hint: Some(open_world),
146            ..self
147        }
148    }
149
150    /// If not set, defaults to true.
151    pub fn is_destructive(&self) -> bool {
152        self.destructive_hint.unwrap_or(true)
153    }
154
155    /// If not set, defaults to false.
156    pub fn is_idempotent(&self) -> bool {
157        self.idempotent_hint.unwrap_or(false)
158    }
159}
160
161impl Tool {
162    /// Create a new tool with the given name and description
163    pub fn new<N, D, S>(name: N, description: D, input_schema: S) -> Self
164    where
165        N: Into<Cow<'static, str>>,
166        D: Into<Cow<'static, str>>,
167        S: Into<Arc<JsonObject>>,
168    {
169        Tool {
170            name: name.into(),
171            title: None,
172            description: Some(description.into()),
173            input_schema: input_schema.into(),
174            output_schema: None,
175            annotations: None,
176            icons: None,
177            meta: None,
178        }
179    }
180
181    /// Create a new tool with just a name and input schema (no description)
182    pub fn new_with_raw<N, S>(
183        name: N,
184        description: Option<Cow<'static, str>>,
185        input_schema: S,
186    ) -> Self
187    where
188        N: Into<Cow<'static, str>>,
189        S: Into<Arc<JsonObject>>,
190    {
191        Tool {
192            name: name.into(),
193            title: None,
194            description,
195            input_schema: input_schema.into(),
196            output_schema: None,
197            annotations: None,
198            icons: None,
199            meta: None,
200        }
201    }
202
203    /// Set the human-readable title
204    pub fn with_title(mut self, title: impl Into<String>) -> Self {
205        self.title = Some(title.into());
206        self
207    }
208
209    /// Set the output schema from a raw value
210    pub fn with_raw_output_schema(mut self, output_schema: Arc<JsonObject>) -> Self {
211        self.output_schema = Some(output_schema);
212        self
213    }
214
215    /// Set the annotations
216    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
217        self.annotations = Some(annotations);
218        self
219    }
220
221    /// Set the icons
222    pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
223        self.icons = Some(icons);
224        self
225    }
226
227    /// Set the metadata
228    pub fn with_meta(mut self, meta: MetaObject) -> Self {
229        self.meta = Some(meta);
230        self
231    }
232
233    pub fn annotate(self, annotations: ToolAnnotations) -> Self {
234        Tool {
235            annotations: Some(annotations),
236            ..self
237        }
238    }
239
240    /// Set the output schema using a type that implements JsonSchema
241    #[cfg(feature = "server")]
242    pub fn with_output_schema<T: JsonSchema + 'static>(mut self) -> Self {
243        self.output_schema = Some(crate::handler::server::tool::schema_for_output::<T>());
244        self
245    }
246
247    /// Set the input schema using a type that implements JsonSchema
248    #[cfg(feature = "server")]
249    pub fn with_input_schema<T: JsonSchema + 'static>(mut self) -> Self {
250        self.input_schema = crate::handler::server::tool::schema_for_input::<T>()
251            .unwrap_or_else(|e| panic!("Invalid input schema for tool '{}': {}", self.name, e));
252        self
253    }
254
255    /// Get the schema as json value
256    pub fn schema_as_json_value(&self) -> Value {
257        Value::Object(self.input_schema.as_ref().clone())
258    }
259}