#[cfg(any(feature = "server", feature = "client"))]
use crate::error::{Error, ErrorCode};
use crate::shared;
use crate::types::{Cursor, Icon, PropertyType, request::RequestParamsMeta};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
#[cfg(feature = "server")]
use {
super::helpers::TypeCategory,
crate::shared::BoxFuture,
crate::types::{
ArgNames, FromHandlerArgs, FromRequest, IntoResponse, Page, Request, RequestId, Response,
},
crate::{
Context,
app::handler::{FromHandlerParams, GenericHandler, Handler, HandlerParams, RequestHandler},
},
std::{future::Future, sync::Arc},
};
#[cfg(all(feature = "server", feature = "legacy-spec"))]
use crate::json::JsonSchema;
#[cfg(all(feature = "server", feature = "tasks"))]
use crate::types::RelatedTaskMetadata;
#[cfg(feature = "tasks")]
use crate::types::TaskMetadata;
#[cfg(feature = "client")]
use jsonschema::validator_for;
pub use call_tool_response::CallToolResponse;
mod call_tool_response;
#[cfg(feature = "server")]
mod from_request;
pub mod commands {
pub const LIST: &str = "tools/list";
pub const LIST_CHANGED: &str = "notifications/tools/list_changed";
pub const CALL: &str = "tools/call";
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(rename = "description", skip_serializing_if = "Option::is_none")]
pub descr: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: crate::types::ToolInputSchema,
#[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
pub output_schema: Option<crate::types::ToolInputSchema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[cfg(feature = "tasks")]
#[serde(rename = "execution", skip_serializing_if = "Option::is_none")]
pub exec: Option<ToolExecution>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
#[serde(skip)]
#[cfg(feature = "http-server")]
pub(crate) roles: Option<Vec<String>>,
#[serde(skip)]
#[cfg(feature = "http-server")]
pub(crate) permissions: Option<Vec<String>>,
#[serde(skip)]
#[cfg(feature = "server")]
handler: Option<RequestHandler<CallToolResponse>>,
#[serde(skip)]
#[cfg(feature = "server")]
pub(crate) arg_names: ArgNames,
#[serde(skip)]
#[cfg(feature = "server")]
custom_schema: bool,
}
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg(feature = "tasks")]
pub struct ToolExecution {
#[serde(rename = "taskSupport", skip_serializing_if = "Option::is_none")]
pub task_support: Option<TaskSupport>,
}
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[cfg(feature = "tasks")]
#[serde(rename_all = "lowercase")]
pub enum TaskSupport {
#[default]
Forbidden,
Optional,
Required,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ListToolsRequestParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<Cursor>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ListToolsResult {
pub tools: Vec<Tool>,
#[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<Cursor>,
#[cfg(not(feature = "legacy-spec"))]
#[serde(rename = "ttlMs", default)]
pub ttl_ms: u64,
#[cfg(not(feature = "legacy-spec"))]
#[serde(rename = "cacheScope", default)]
pub cache_scope: crate::types::CacheScope,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolRequestParams {
pub name: String,
#[serde(rename = "arguments", default, skip_serializing_if = "Option::is_none")]
pub args: Option<HashMap<String, Value>>,
#[cfg(feature = "tasks")]
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<TaskMetadata>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestParamsMeta>,
}
#[cfg(feature = "legacy-spec")]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolSchema {
#[serde(rename = "type", default)]
pub r#type: PropertyType,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, SchemaProperty>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchemaProperty {
#[serde(
rename = "type",
default = "PropertyType::unstated",
skip_serializing_if = "PropertyType::is_unstated"
)]
pub r#type: PropertyType,
#[serde(rename = "description", skip_serializing_if = "Option::is_none")]
pub descr: Option<String>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, Value>,
}
#[cfg(feature = "server")]
#[derive(Debug, Clone)]
pub struct ToolArg {
pub property: SchemaProperty,
pub required: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
pub destructive: Option<bool>,
#[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
pub idempotent: Option<bool>,
#[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
pub open_world: Option<bool>,
#[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
pub readonly: Option<bool>,
}
#[cfg(feature = "server")]
impl IntoResponse for ListToolsResult {
#[inline]
fn into_response(self, req_id: RequestId) -> Response {
match serde_json::to_value(self) {
Ok(v) => Response::success(req_id, v),
Err(err) => Response::error(req_id, err.into()),
}
}
}
#[cfg(feature = "server")]
impl From<Vec<Tool>> for ListToolsResult {
#[inline]
#[cfg_attr(feature = "legacy-spec", allow(clippy::needless_update))]
fn from(tools: Vec<Tool>) -> Self {
Self {
next_cursor: None,
tools,
..Default::default()
}
}
}
#[cfg(feature = "server")]
impl From<Page<'_, Tool>> for ListToolsResult {
#[inline]
#[cfg_attr(feature = "legacy-spec", allow(clippy::needless_update))]
fn from(page: Page<'_, Tool>) -> Self {
Self {
next_cursor: page.next_cursor,
tools: page.items.to_vec(),
..Default::default()
}
}
}
#[cfg(feature = "server")]
impl ListToolsResult {
#[inline]
pub fn new() -> Self {
Default::default()
}
}
#[cfg(feature = "client")]
impl ListToolsResult {
#[inline]
pub fn get(&self, name: impl AsRef<str>) -> Option<&Tool> {
self.get_by(|t| t.name == name.as_ref())
}
#[inline]
pub fn get_by<F>(&self, mut f: F) -> Option<&Tool>
where
F: FnMut(&Tool) -> bool,
{
self.tools.iter().find(|&t| f(t))
}
}
#[cfg(feature = "legacy-spec")]
impl Default for ToolSchema {
#[inline]
fn default() -> Self {
Self {
r#type: PropertyType::Object,
properties: Some(HashMap::new()),
required: None,
extra: Default::default(),
}
}
}
impl Default for ToolAnnotations {
#[inline]
fn default() -> Self {
Self {
title: None,
destructive: Some(true),
idempotent: Some(false),
open_world: Some(true),
readonly: Some(false),
}
}
}
#[cfg(feature = "tasks")]
impl From<&str> for TaskSupport {
#[inline]
fn from(value: &str) -> Self {
match value {
"forbidden" => Self::Forbidden,
"required" => Self::Required,
"optional" => Self::Optional,
_ => unreachable!(),
}
}
}
#[cfg(feature = "tasks")]
impl From<String> for TaskSupport {
#[inline]
fn from(value: String) -> Self {
Self::from(value.as_str())
}
}
#[cfg(all(feature = "server", feature = "legacy-spec"))]
impl ToolSchema {
#[inline]
pub(crate) fn new(
props: Option<HashMap<String, SchemaProperty>>,
required: Option<Vec<String>>,
) -> Self {
Self {
r#type: PropertyType::Object,
properties: props,
required,
extra: Default::default(),
}
}
#[inline]
pub fn from_json_str(json: &str) -> Self {
serde_json::from_str(json).expect("InputSchema: Incorrect JSON string provided")
}
#[inline]
pub fn from_value(value: Value) -> Result<Self, crate::error::Error> {
let schema = serde_json::from_value(value)?;
Ok(schema)
}
pub fn with_prop<T: Into<PropertyType>>(
self,
name: &str,
descr: &str,
property_type: T,
) -> Self {
self.add_property_impl(name, descr, property_type.into())
}
pub fn with_required<T: Into<PropertyType>>(
self,
name: &str,
descr: &str,
property_type: T,
) -> Self {
self.add_required_property_impl(name, descr, property_type.into())
}
pub fn with_schema<T: JsonSchema>(self) -> Self {
let json_schema = schemars::schema_for!(T);
self.with_schema_impl(json_schema)
}
#[inline]
pub fn from_schema<T: JsonSchema>() -> Self {
let json_schema = schemars::schema_for!(T);
Self::from_schemars(json_schema)
}
#[inline]
pub fn from_schemars(json_schema: schemars::Schema) -> Self {
Self::default().with_schema_impl(json_schema)
}
#[deprecated(note = "renamed to from_schemars for symmetry with InputSchema")]
#[inline]
pub fn from_schema_legacy(json_schema: schemars::Schema) -> Self {
Self::from_schemars(json_schema)
}
#[inline]
fn with_schema_impl(mut self, json_schema: schemars::Schema) -> Self {
let required = json_schema.get("required").and_then(|v| v.as_array());
if let Some(props) = json_schema.get("properties").and_then(|v| v.as_object()) {
for (field, def) in props {
let req = required
.map(|arr| !arr.iter().any(|v| v == field))
.unwrap_or(true);
let type_str = def.get("type").and_then(|v| v.as_str()).unwrap_or("string");
self = if req {
self.add_required_property_impl(field, field, type_str.into())
} else {
self.add_property_impl(field, field, type_str.into())
};
}
}
self
}
#[inline]
fn add_property_impl(mut self, name: &str, descr: &str, property_type: PropertyType) -> Self {
self.properties.get_or_insert_with(HashMap::new).insert(
name.into(),
SchemaProperty {
r#type: property_type,
descr: Some(descr.into()),
extra: Default::default(),
},
);
self
}
#[inline]
fn add_required_property_impl(
mut self,
name: &str,
descr: &str,
property_type: PropertyType,
) -> Self {
self = self.add_property_impl(name, descr, property_type);
self.required.get_or_insert_with(Vec::new).push(name.into());
self
}
}
#[cfg(feature = "server")]
impl SchemaProperty {
#[inline]
pub(crate) fn new<T: TypeCategory>() -> Self {
Self {
r#type: T::category(),
descr: None,
extra: Default::default(),
}
}
}
#[cfg(feature = "server")]
impl FromHandlerParams for CallToolRequestParams {
#[inline]
fn from_params(params: &HandlerParams) -> Result<Self, Error> {
let req = Request::from_params(params)?;
Self::from_request(req)
}
}
#[cfg(feature = "server")]
impl FromHandlerParams for ListToolsRequestParams {
#[inline]
fn from_params(params: &HandlerParams) -> Result<Self, Error> {
let req = Request::from_params(params)?;
Self::from_request(req)
}
}
#[cfg(feature = "server")]
pub trait ToolHandler<Args>: GenericHandler<Args> {
#[inline]
fn args() -> Vec<ToolArg> {
Vec::new()
}
}
#[cfg(feature = "server")]
pub(crate) struct ToolFunc<F, R, Args>
where
F: ToolHandler<Args, Output = R>,
R: Into<CallToolResponse>,
Args: FromHandlerArgs<CallToolRequestParams>,
{
func: F,
_marker: std::marker::PhantomData<Args>,
}
#[cfg(feature = "server")]
impl<F, R, Args> ToolFunc<F, R, Args>
where
F: ToolHandler<Args, Output = R>,
R: Into<CallToolResponse>,
Args: FromHandlerArgs<CallToolRequestParams>,
{
pub(crate) fn new(func: F) -> Arc<Self> {
let func = Self {
func,
_marker: std::marker::PhantomData,
};
Arc::new(func)
}
}
#[cfg(feature = "server")]
impl<F, R, Args> Handler<CallToolResponse> for ToolFunc<F, R, Args>
where
F: ToolHandler<Args, Output = R>,
R: Into<CallToolResponse>,
Args: FromHandlerArgs<CallToolRequestParams> + Send + Sync,
{
#[inline]
fn call(&self, params: HandlerParams) -> BoxFuture<'_, Result<CallToolResponse, Error>> {
let HandlerParams::Tool(params, names) = params else {
unreachable!()
};
Box::pin(async move {
let args = Args::from_args(params, &names)?;
Ok(self.func.call(args).await.into())
})
}
}
impl CallToolRequestParams {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
args: None,
meta: None,
#[cfg(feature = "tasks")]
task: None,
}
}
pub fn with_args<Args: shared::IntoArgs>(mut self, args: Args) -> Self {
self.args = args.into_args();
self
}
pub fn with_meta(mut self, meta: RequestParamsMeta) -> Self {
self.meta = Some(meta);
self
}
#[cfg(feature = "tasks")]
pub fn with_ttl(mut self, ttl: Option<usize>) -> Self {
self.task = Some(TaskMetadata { ttl });
self
}
}
#[cfg(feature = "server")]
impl CallToolRequestParams {
pub(crate) fn with_context(mut self, ctx: Context) -> Self {
self.meta.get_or_insert_default().context = Some(ctx);
self
}
#[cfg(feature = "tasks")]
pub(crate) fn with_task(mut self, task_id: impl Into<String>) -> Self {
self.meta.get_or_insert_default().task = Some(RelatedTaskMetadata { id: task_id.into() });
self
}
}
impl Debug for Tool {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tool")
.field("name", &self.name)
.field("title", &self.title)
.field("descr", &self.descr)
.field("input_schema", &self.input_schema)
.field("output_schema", &self.output_schema)
.field("annotations", &self.annotations)
.field("meta", &self.meta)
.finish()
}
}
#[cfg(feature = "server")]
#[inline]
fn build_input_schema_from_args(
args: &[ToolArg],
names: &ArgNames,
) -> crate::types::ToolInputSchema {
#[cfg(feature = "legacy-spec")]
{
if args.is_empty() {
return ToolSchema::new(None, None);
}
let props = args
.iter()
.enumerate()
.map(|(idx, arg)| (names.get(idx).to_owned(), arg.property.clone()))
.collect::<HashMap<_, _>>();
let required = args
.iter()
.enumerate()
.filter(|(_, arg)| arg.required)
.map(|(idx, _)| names.get(idx).to_owned())
.collect::<Vec<_>>();
let required = (!required.is_empty()).then_some(required);
ToolSchema::new(Some(props), required)
}
#[cfg(not(feature = "legacy-spec"))]
{
use serde_json::{Map, Value, json};
let mut properties = Map::with_capacity(args.len());
let mut required = Vec::with_capacity(args.len());
for (idx, arg) in args.iter().enumerate() {
let name = names.get(idx);
let prop =
serde_json::to_value(&arg.property).unwrap_or_else(|_| Value::Object(Map::new()));
properties.insert(name.to_owned(), prop);
if arg.required {
required.push(Value::String(name.to_owned()));
}
}
let value = if required.is_empty() {
json!({ "type": "object", "properties": properties })
} else {
json!({ "type": "object", "properties": properties, "required": required })
};
crate::types::schema_2020::InputSchema::from(value)
}
}
#[cfg(all(feature = "server", not(feature = "legacy-spec")))]
fn advertises_properties_elsewhere(schema: &serde_json::Map<String, Value>) -> bool {
const ADVERTISES: [&str; 9] = [
"$ref",
"allOf",
"anyOf",
"oneOf",
"if",
"then",
"else",
"dependentSchemas",
"patternProperties",
];
ADVERTISES.iter().any(|kw| schema.contains_key(*kw))
}
#[cfg(feature = "server")]
#[inline]
fn rename_args(schema: &mut crate::types::ToolInputSchema, from: &ArgNames, to: &ArgNames) {
#[cfg(feature = "legacy-spec")]
{
if let Some(props) = schema.properties.as_mut() {
let taken = (0..to.len())
.map(|slot| props.remove(from.get(slot)))
.collect::<Vec<_>>();
for (slot, prop) in taken.into_iter().enumerate() {
if let Some(prop) = prop {
props.insert(to.get(slot).to_owned(), prop);
}
}
}
if let Some(required) = schema.required.as_mut() {
for name in required.iter_mut() {
if let Some(slot) = from.slot_of(name) {
*name = to.get(slot).to_owned();
}
}
}
}
#[cfg(not(feature = "legacy-spec"))]
{
let Some(schema) = schema.0.as_object_mut() else {
return;
};
if let Some(props) = schema.get_mut("properties").and_then(Value::as_object_mut) {
let taken = (0..to.len())
.map(|slot| props.remove(from.get(slot)))
.collect::<Vec<_>>();
for (slot, prop) in taken.into_iter().enumerate() {
if let Some(prop) = prop {
props.insert(to.get(slot).to_owned(), prop);
}
}
}
if let Some(required) = schema.get_mut("required").and_then(Value::as_array_mut) {
for name in required.iter_mut() {
if let Some(slot) = name.as_str().and_then(|name| from.slot_of(name)) {
*name = Value::String(to.get(slot).to_owned());
}
}
}
}
}
#[cfg(feature = "server")]
impl Tool {
pub fn new<F, Args, R>(name: impl Into<String>, handler: F) -> Self
where
F: ToolHandler<Args, Output = R>,
R: Into<CallToolResponse> + Send + 'static,
Args: FromHandlerArgs<CallToolRequestParams> + Send + Sync + 'static,
{
let handler = ToolFunc::new(handler);
let args = F::args();
let arg_names = ArgNames::positional(args.len());
let input_schema = build_input_schema_from_args(&args, &arg_names);
Self {
name: name.into(),
title: None,
descr: None,
input_schema,
output_schema: None,
meta: None,
annotations: None,
handler: Some(handler),
arg_names,
custom_schema: false,
icons: None,
#[cfg(feature = "http-server")]
roles: None,
#[cfg(feature = "http-server")]
permissions: None,
#[cfg(feature = "tasks")]
exec: None,
}
}
pub fn with_title(&mut self, title: impl Into<String>) -> &mut Self {
self.title = Some(title.into());
self
}
pub fn with_description(&mut self, description: &str) -> &mut Self {
self.descr = Some(description.into());
self
}
pub fn with_input_schema<F>(&mut self, config: F) -> &mut Self
where
F: FnOnce(crate::types::ToolInputSchema) -> crate::types::ToolInputSchema,
{
self.input_schema = config(Default::default());
self.custom_schema = true;
self
}
pub fn with_arg_names<T, I>(&mut self, names: T) -> &mut Self
where
T: IntoIterator<Item = I>,
I: Into<String>,
{
let declared = self.arg_names.declare(names);
if !self.custom_schema {
rename_args(&mut self.input_schema, &self.arg_names, &declared);
}
self.arg_names = declared;
self
}
pub fn with_output_schema<F>(&mut self, config: F) -> &mut Self
where
F: FnOnce(crate::types::ToolInputSchema) -> crate::types::ToolInputSchema,
{
self.output_schema = Some(config(Default::default()));
self
}
#[cfg(feature = "http-server")]
pub fn with_roles<T, I>(&mut self, roles: T) -> &mut Self
where
T: IntoIterator<Item = I>,
I: Into<String>,
{
self.roles = Some(roles.into_iter().map(Into::into).collect());
self
}
#[cfg(feature = "http-server")]
pub fn with_permissions<T, I>(&mut self, permissions: T) -> &mut Self
where
T: IntoIterator<Item = I>,
I: Into<String>,
{
self.permissions = Some(permissions.into_iter().map(Into::into).collect());
self
}
pub fn with_annotations<F>(&mut self, config: F) -> &mut Self
where
F: FnOnce(ToolAnnotations) -> ToolAnnotations,
{
self.annotations = Some(config(Default::default()));
self
}
pub fn with_icons(&mut self, icons: impl IntoIterator<Item = Icon>) -> &mut Self {
self.icons = Some(icons.into_iter().collect());
self
}
#[cfg(feature = "tasks")]
pub fn with_task_support(&mut self, support: impl Into<TaskSupport>) -> &mut Self {
self.exec = Some(ToolExecution::new(support.into()));
self
}
pub(crate) fn arg_name_conflict(&self) -> Option<String> {
let arity = self.arg_names.arity();
if self.arg_names.is_declared() {
let declared = self.arg_names.len();
if declared != arity {
return Some(format!(
"tool `{}` declares {declared} argument name(s) but its handler takes \
{arity}. Name every argument the handler reads, metadata parameters \
(`Context`, `Meta<_>`, `Dc<_>`) excluded.",
self.name,
));
}
if let Some(duplicate) = self.arg_names.duplicate() {
return Some(format!(
"tool `{}` declares the argument name `{duplicate}` twice. Arguments are \
read from a call by name, so two parameters sharing one name would both \
be handed the same value.",
self.name,
));
}
}
if arity == 0 {
return None;
}
let properties = self.schema_properties()?;
let missing = (0..arity)
.map(|slot| self.arg_names.get(slot))
.find(|name| !properties(name))?;
Some(if self.arg_names.is_declared() {
format!(
"tool `{}` declares the argument `{missing}` but publishes an inputSchema \
without it. A peer sends what the schema asks for, so the two have to name \
the same arguments: either rename the schema property, or pass the schema's \
own names to `.with_arg_names([...])`.",
self.name,
)
} else {
format!(
"tool `{}` publishes an inputSchema without the argument `{missing}` that its \
handler reads. A tool registered from a closure has no argument names -- Rust \
does not keep a closure's parameter names -- so it reads the positional `arg0`, \
`arg1`, ... keys, and replacing its schema renamed only what peers are told to \
send. Declare the names with `.with_arg_names([...])`, or register the tool with \
the `map_tool!` macro or the `#[tool]` attribute.",
self.name,
)
})
}
#[inline]
fn schema_properties(&self) -> Option<impl Fn(&str) -> bool + '_> {
#[cfg(feature = "legacy-spec")]
let props = self.input_schema.properties.as_ref()?;
#[cfg(not(feature = "legacy-spec"))]
let props = {
let schema = self.input_schema.as_value().as_object()?;
if advertises_properties_elsewhere(schema) {
return None;
}
schema.get("properties").and_then(Value::as_object)?
};
Some(move |name: &str| props.contains_key(name))
}
#[inline]
pub(crate) async fn call(
&self,
params: CallToolRequestParams,
) -> Result<CallToolResponse, Error> {
match self.handler {
Some(ref handler) => {
handler
.call(HandlerParams::Tool(params, self.arg_names.clone()))
.await
}
None => Err(Error::new(
ErrorCode::InternalError,
"Tool handler not specified",
)),
}
}
}
#[cfg(feature = "client")]
impl Tool {
pub fn validate<'a>(&self, resp: &'a CallToolResponse) -> Result<&'a CallToolResponse, Error> {
let Some(schema_ref) = self.output_schema.as_ref() else {
return Err(Error::new(
ErrorCode::ParseError,
"Tool: Output schema not specified",
));
};
#[cfg(feature = "legacy-spec")]
let schema = serde_json::to_value(schema_ref).map_err(Into::<Error>::into)?;
#[cfg(not(feature = "legacy-spec"))]
let schema = schema_ref.as_value().clone();
let validator =
validator_for(&schema).map_err(|err| Error::new(ErrorCode::ParseError, err))?;
let content = resp.struct_content()?;
validator
.validate(content)
.map(|_| resp)
.map_err(|err| Error::new(ErrorCode::ParseError, err.to_string()))
}
}
#[cfg(feature = "tasks")]
impl Tool {
#[inline]
pub fn task_support(&self) -> Option<TaskSupport> {
self.exec.as_ref().and_then(|e| e.task_support)
}
}
#[cfg(feature = "server")]
impl ToolAnnotations {
#[inline]
pub fn new() -> Self {
Default::default()
}
#[inline]
pub fn from_json_str(json: &str) -> Self {
serde_json::from_str(json).expect("ToolAnnotations: Incorrect JSON string provided")
}
#[inline]
pub fn with_title(mut self, title: &str) -> Self {
self.title = Some(title.into());
self
}
#[inline]
pub fn with_destructive(mut self, destructive: bool) -> Self {
self.destructive = Some(destructive);
self.readonly = Some(false);
self
}
pub fn with_idempotent(mut self, idempotent: bool) -> Self {
self.idempotent = Some(idempotent);
self.readonly = Some(false);
self
}
#[inline]
pub fn with_open_world(mut self, open_world: bool) -> Self {
self.open_world = Some(open_world);
self
}
}
#[cfg(all(feature = "server", feature = "tasks"))]
impl ToolExecution {
#[inline]
pub fn new(support: TaskSupport) -> Self {
Self {
task_support: Some(support),
}
}
}
macro_rules! impl_generic_tool_handler ({ $($param:ident)* } => {
#[cfg(feature = "server")]
impl<Func, Fut: Send, $($param: TypeCategory,)*> ToolHandler<($($param,)*)> for Func
where
Func: Fn($($param),*) -> Fut + Send + Sync + Clone + 'static,
Fut: Future + 'static,
{
#[inline]
#[allow(unused_mut)]
fn args() -> Vec<ToolArg> {
let mut args = Vec::new();
$(
{
let property = SchemaProperty::new::<$param>();
if property.r#type != PropertyType::None {
args.push(ToolArg {
property,
required: !<$param as TypeCategory>::is_optional(),
});
}
};
)*
args
}
}
});
impl_generic_tool_handler! {}
impl_generic_tool_handler! { T1 }
impl_generic_tool_handler! { T1 T2 }
impl_generic_tool_handler! { T1 T2 T3 }
impl_generic_tool_handler! { T1 T2 T3 T4 }
impl_generic_tool_handler! { T1 T2 T3 T4 T5 }
#[cfg(test)]
#[cfg(feature = "server")]
mod tests {
use super::*;
use serde_json::json;
fn call_params(args: [(&str, Value); 2]) -> CallToolRequestParams {
CallToolRequestParams {
name: "sum".into(),
meta: None,
#[cfg(feature = "tasks")]
task: None,
args: Some(args.into_iter().map(|(k, v)| (k.to_owned(), v)).collect()),
}
}
fn schema_props(tool: &Tool) -> Vec<String> {
#[cfg(feature = "legacy-spec")]
let props = tool.input_schema.properties.as_ref().unwrap();
#[cfg(not(feature = "legacy-spec"))]
let props = tool.input_schema.as_value()["properties"]
.as_object()
.unwrap();
let mut names = props.keys().cloned().collect::<Vec<_>>();
names.sort();
names
}
#[tokio::test]
async fn it_creates_and_calls_tool() {
let tool = Tool::new("sum", |a: i32, b: i32| async move { a + b });
assert_eq!(schema_props(&tool), ["arg0", "arg1"]);
let params = call_params([("arg0", json!(5)), ("arg1", json!(2))]);
let resp = tool.call(params).await.unwrap();
let json = serde_json::to_string(&resp).unwrap();
assert_eq!(
json,
r#"{"content":[{"type":"text","text":"7"}],"isError":false}"#
);
}
#[tokio::test]
async fn it_calls_a_tool_with_declared_arg_names() {
let mut tool = Tool::new("sum", |a: i32, b: i32| async move { a - b });
tool.with_arg_names(["a", "b"]);
assert_eq!(schema_props(&tool), ["a", "b"]);
let params = call_params([("b", json!(2)), ("a", json!(5))]);
let resp = tool.call(params).await.unwrap();
let json = serde_json::to_string(&resp).unwrap();
assert_eq!(
json,
r#"{"content":[{"type":"text","text":"3"}],"isError":false}"#
);
}
#[tokio::test]
async fn it_does_not_swap_same_typed_args_of_different_types() {
let mut tool = Tool::new("greet", |name: String, age: i32| async move {
format!("{name} is {age}")
});
tool.with_arg_names(["name", "age"]);
for args in [
[("name", json!("John")), ("age", json!(30))],
[("age", json!(30)), ("name", json!("John"))],
] {
let resp = tool.call(call_params(args)).await.unwrap();
let json = serde_json::to_string(&resp).unwrap();
assert_eq!(
json,
r#"{"content":[{"type":"text","text":"John is 30"}],"isError":false}"#
);
}
}
fn schema_required(tool: &Tool) -> Vec<String> {
#[cfg(feature = "legacy-spec")]
let required = tool.input_schema.required.clone().unwrap_or_default();
#[cfg(not(feature = "legacy-spec"))]
let required = tool.input_schema.as_value()["required"]
.as_array()
.map(|items| {
items
.iter()
.filter_map(|item| item.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
let mut required: Vec<String> = required;
required.sort();
required
}
#[tokio::test]
async fn it_publishes_an_optional_arg_as_not_required() {
let mut tool = Tool::new("greet", |name: String, age: Option<i32>| async move {
match age {
Some(age) => format!("{name} is {age}"),
None => format!("{name} is ageless"),
}
});
tool.with_arg_names(["name", "age"]);
assert_eq!(schema_props(&tool), ["age", "name"]);
assert_eq!(schema_required(&tool), ["name"]);
let supplied = call_params([("name", json!("John")), ("age", json!(30))]);
let resp = tool.call(supplied).await.unwrap();
assert!(serde_json::to_string(&resp).unwrap().contains("John is 30"));
let omitted = CallToolRequestParams {
name: "greet".into(),
meta: None,
#[cfg(feature = "tasks")]
task: None,
args: Some(HashMap::from([("name".to_owned(), json!("John"))])),
};
let resp = tool.call(omitted).await.unwrap();
assert!(
serde_json::to_string(&resp)
.unwrap()
.contains("John is ageless")
);
}
#[tokio::test]
async fn it_reads_an_explicit_null_as_an_absent_optional_arg() {
let mut tool = Tool::new(
"greet",
|age: Option<i32>| async move { format!("{age:?}") },
);
tool.with_arg_names(["age"]);
let resp = tool
.call(CallToolRequestParams {
name: "greet".into(),
meta: None,
#[cfg(feature = "tasks")]
task: None,
args: Some(HashMap::from([("age".to_owned(), Value::Null)])),
})
.await
.unwrap();
assert!(serde_json::to_string(&resp).unwrap().contains("None"));
}
#[test]
fn an_all_optional_tool_requires_nothing() {
let tool = Tool::new("greet", |name: Option<String>| async move {
name.unwrap_or_default()
});
assert_eq!(schema_props(&tool), ["arg0"]);
assert!(schema_required(&tool).is_empty());
}
#[test]
fn it_gives_same_typed_args_distinct_schema_properties() {
let tool = Tool::new("sum", |a: i32, b: i32| async move { a + b });
assert_eq!(schema_props(&tool).len(), 2);
}
#[test]
fn it_renames_a_parameter_that_is_itself_named_after_a_slot() {
let mut tool = Tool::new("f", |arg1: String, other: String| async move {
format!("{arg1}{other}")
});
tool.with_arg_names(["arg1", "other"]);
assert_eq!(schema_props(&tool), ["arg1", "other"]);
assert_eq!(schema_required(&tool), ["arg1", "other"]);
assert!(tool.arg_name_conflict().is_none());
}
#[test]
fn it_renames_when_a_declared_name_reuses_a_later_slot() {
let mut tool = Tool::new("f", |other: String, arg0: String| async move {
format!("{other}{arg0}")
});
tool.with_arg_names(["other", "arg0"]);
assert_eq!(schema_props(&tool), ["arg0", "other"]);
assert!(tool.arg_name_conflict().is_none());
}
#[tokio::test]
async fn it_renames_again_when_names_are_redeclared() {
let mut tool = Tool::new(
"greet",
|a: String, b: i32| async move { format!("{a}{b}") },
);
tool.with_arg_names(["name", "age"]);
tool.with_arg_names(["who", "years"]);
assert_eq!(schema_props(&tool), ["who", "years"]);
assert_eq!(schema_required(&tool), ["who", "years"]);
assert!(tool.arg_name_conflict().is_none());
let resp = tool
.call(call_params([("years", json!(30)), ("who", json!("John"))]))
.await
.unwrap();
assert!(serde_json::to_string(&resp).unwrap().contains("John30"));
}
#[test]
fn it_swaps_declared_names_without_losing_a_property() {
let mut tool = Tool::new("f", |a: String, b: String| async move { format!("{a}{b}") });
tool.with_arg_names(["first", "second"]);
tool.with_arg_names(["second", "first"]);
assert_eq!(schema_props(&tool), ["first", "second"]);
assert!(tool.arg_name_conflict().is_none());
}
#[test]
fn it_rejects_a_duplicate_declared_name() {
let mut tool = Tool::new("f", |a: String, b: String| async move { format!("{a}{b}") });
tool.with_arg_names(["value", "value"]);
let conflict = tool.arg_name_conflict().expect("must be reported");
assert!(
conflict.contains("declares the argument name `value` twice"),
"unexpected conflict: {conflict}"
);
}
#[test]
fn it_leaves_a_hand_written_schema_untouched() {
let mut tool = Tool::new("sum", |a: i32, b: i32| async move { a + b });
tool.with_input_schema(|_| {
#[cfg(feature = "legacy-spec")]
{
ToolSchema::from_json_str(
r#"{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}}}"#,
)
}
#[cfg(not(feature = "legacy-spec"))]
{
crate::types::schema_2020::InputSchema::from(json!({
"type": "object",
"properties": { "a": { "type": "number" }, "b": { "type": "number" } }
}))
}
})
.with_arg_names(["a", "b"]);
assert_eq!(schema_props(&tool), ["a", "b"]);
}
#[test]
fn it_leaves_a_positional_looking_property_of_a_hand_written_schema_alone() {
let mut tool = Tool::new("sum", |a: i32, b: i32| async move { a + b });
tool.with_input_schema(|_| {
const JSON: &str = r#"{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "number" },
"arg0": { "type": "boolean" }
}
}"#;
#[cfg(feature = "legacy-spec")]
{
ToolSchema::from_json_str(JSON)
}
#[cfg(not(feature = "legacy-spec"))]
{
crate::types::schema_2020::InputSchema::from_json_str(JSON).unwrap_or_default()
}
})
.with_arg_names(["name", "age"]);
assert_eq!(schema_props(&tool), ["age", "arg0", "name"]);
#[cfg(feature = "legacy-spec")]
{
let props = tool.input_schema.properties.as_ref().unwrap();
assert_eq!(props["name"].r#type, PropertyType::String);
assert_eq!(props["arg0"].r#type, PropertyType::Bool);
}
#[cfg(not(feature = "legacy-spec"))]
{
let props = &tool.input_schema.as_value()["properties"];
assert_eq!(props["name"]["type"], "string");
assert_eq!(props["arg0"]["type"], "boolean");
}
assert!(tool.arg_name_conflict().is_none());
}
#[test]
#[cfg(feature = "legacy-spec")]
fn a_property_keeps_the_keywords_it_was_declared_with() {
let declared = serde_json::json!({
"type": "object",
"properties": {
"address": { "$ref": "#/$defs/address" },
"contactMethod": { "type": "string", "enum": ["phone", "email"] },
"age": { "type": "integer", "minimum": 0, "maximum": 130 }
}
});
let schema: ToolSchema =
serde_json::from_value(declared.clone()).expect("a legacy tool schema");
let republished = serde_json::to_value(&schema).expect("serializable");
assert_eq!(
republished["properties"], declared["properties"],
"every property must come back out as it went in, got: {republished}"
);
assert_eq!(
republished["properties"]["address"],
serde_json::json!({ "$ref": "#/$defs/address" }),
"an untyped property must not acquire a type"
);
assert_eq!(
republished["properties"]["contactMethod"]["enum"],
serde_json::json!(["phone", "email"]),
"the values a property is limited to are the point of declaring it"
);
}
#[test]
#[cfg(feature = "legacy-spec")]
fn it_deserializes_input_schema() {
let json = r#"{
"properties": {
"name": {
"type": "string",
"description": "A name to whom say hello"
}
}
}"#;
let schema: ToolSchema = serde_json::from_str(json).unwrap();
assert_eq!(schema.r#type, PropertyType::Object);
assert!(schema.properties.is_some());
}
#[cfg(feature = "legacy-spec")]
#[derive(serde::Deserialize, schemars::JsonSchema)]
#[allow(dead_code)]
struct MyT {
name: String,
}
#[test]
#[cfg(feature = "legacy-spec")]
#[allow(deprecated)]
fn from_schemars_matches_from_schema_legacy_name() {
let a = ToolSchema::from_schemars(schemars::schema_for!(MyT));
let b = ToolSchema::from_schema_legacy(schemars::schema_for!(MyT));
let av = serde_json::to_value(&a).unwrap();
let bv = serde_json::to_value(&b).unwrap();
assert_eq!(av, bv);
}
#[test]
#[cfg(feature = "legacy-spec")]
fn from_schema_generic_constructor_works() {
let s: ToolSchema = ToolSchema::from_schema::<MyT>();
let props = s.properties.expect("properties should be set");
assert!(!props.is_empty(), "expected at least one property");
assert!(props.contains_key("name"));
}
#[test]
#[cfg(feature = "legacy-spec")]
fn from_value_round_trip() {
let original = ToolSchema::default().with_prop("name", "a name", PropertyType::String);
let value = serde_json::to_value(&original).expect("serializes");
let round_tripped = ToolSchema::from_value(value).expect("round trips");
let a = serde_json::to_value(&original).expect("serializes original");
let b = serde_json::to_value(&round_tripped).expect("serializes round trip");
assert_eq!(a, b);
}
#[test]
#[cfg(feature = "legacy-spec")]
fn from_value_invalid_returns_error() {
let result = ToolSchema::from_value(serde_json::Value::String("not a schema".into()));
assert!(result.is_err(), "expected Err for non-object value");
}
#[test]
#[cfg(not(feature = "legacy-spec"))]
fn list_tools_result_always_carries_cache_fields() {
use crate::types::CacheScope;
let v = serde_json::to_value(ListToolsResult::default()).unwrap();
assert_eq!(v["ttlMs"], serde_json::json!(0));
assert_eq!(v["cacheScope"], serde_json::json!("private"));
let r = ListToolsResult {
ttl_ms: 60_000,
cache_scope: CacheScope::Public,
..Default::default()
};
let v = serde_json::to_value(&r).unwrap();
assert_eq!(v["ttlMs"], serde_json::json!(60_000));
assert_eq!(v["cacheScope"], serde_json::json!("public"));
let back: ListToolsResult =
serde_json::from_value(serde_json::json!({ "tools": [] })).unwrap();
assert_eq!(back.ttl_ms, 0);
assert_eq!(back.cache_scope, CacheScope::Private);
}
}