use std::{collections::HashMap, fmt, ops::Index};
use base64::Engine;
use rust_mcp_schema::{
AudioContent, BlobResourceContents, CallToolResult, ContentBlock, EmbeddedResource,
ImageContent, ResourceLink, TextResourceContents, ToolInputSchema,
};
use serde::Serialize;
use crate::JsonRpcError;
pub trait ToolDef: schemars::JsonSchema + serde::de::DeserializeOwned + 'static {
const NAME: &'static str;
const DESCRIPTION: &'static str;
}
#[derive(Debug, Default)]
pub struct ToolOutput {
content: Vec<ContentBlock>,
structured_content: Option<serde_json::Map<String, serde_json::Value>>,
}
impl ToolOutput {
pub fn new() -> Self {
Self::default()
}
pub fn json<T: Serialize>(value: &T) -> Self {
let text = serde_json::to_string(value).expect("serialization failed");
Self::new().text(text).structured(value)
}
pub fn text<I: Into<String>>(mut self, text: I) -> Self {
self.content.push(ContentBlock::text_content(text.into()));
self
}
pub fn image<S: Into<String>>(mut self, data: &[u8], mime_type: S) -> Self {
let encoded = base64::engine::general_purpose::STANDARD.encode(data);
self.content
.push(ImageContent::new(encoded, mime_type.into(), None, None).into());
self
}
pub fn audio<S: Into<String>>(mut self, data: &[u8], mime_type: S) -> Self {
let encoded = base64::engine::general_purpose::STANDARD.encode(data);
self.content
.push(AudioContent::new(encoded, mime_type.into(), None, None).into());
self
}
pub fn embedded_blob<U: Into<String>, M: Into<String>>(
mut self,
data: &[u8],
uri: U,
mime_type: M,
) -> Self {
let encoded = base64::engine::general_purpose::STANDARD.encode(data);
let blob = BlobResourceContents {
blob: encoded,
uri: uri.into(),
mime_type: Some(mime_type.into()),
meta: None,
};
self.content
.push(EmbeddedResource::new(blob.into(), None, None).into());
self
}
pub fn embedded_text<T: Into<String>, U: Into<String>, M: Into<String>>(
mut self,
text: T,
uri: U,
mime_type: Option<M>,
) -> Self {
let text_resource = TextResourceContents {
text: text.into(),
uri: uri.into(),
mime_type: mime_type.map(Into::into),
meta: None,
};
self.content
.push(EmbeddedResource::new(text_resource.into(), None, None).into());
self
}
pub fn resource_link<U: Into<String>, N: Into<String>>(mut self, uri: U, name: N) -> Self {
self.content.push(
ResourceLink::new(name.into(), uri.into(), None, None, None, None, None, None).into(),
);
self
}
pub fn content(mut self, block: impl Into<ContentBlock>) -> Self {
self.content.push(block.into());
self
}
pub fn structured<T: Serialize>(mut self, value: &T) -> Self {
let json_value = serde_json::to_value(value).expect("serialization failed");
if let serde_json::Value::Object(map) = json_value {
self.structured_content = Some(map);
}
self
}
pub fn content_blocks(&self) -> &[ContentBlock] {
&self.content
}
pub fn structured_content(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.structured_content.as_ref()
}
pub fn as_text(&self) -> Option<&str> {
match self.content.as_slice() {
[ContentBlock::TextContent(text)] => Some(&text.text),
_ => None,
}
}
fn into_call_result(self, is_error: bool) -> CallToolResult {
CallToolResult {
content: self.content,
is_error: Some(is_error),
structured_content: self.structured_content,
meta: None,
}
}
}
impl fmt::Display for ToolOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, block) in self.content.iter().enumerate() {
if i > 0 {
writeln!(f)?;
writeln!(f)?;
}
match block {
ContentBlock::TextContent(text) => {
write!(f, "{}", text.text)?;
}
ContentBlock::ImageContent(img) => {
write!(f, "[Image: {}, {} bytes]", img.mime_type, img.data.len())?;
}
ContentBlock::AudioContent(audio) => {
write!(
f,
"[Audio: {}, {} bytes]",
audio.mime_type,
audio.data.len()
)?;
}
ContentBlock::ResourceLink(link) => {
write!(f, "[Resource: {} ({})]", link.name, link.uri)?;
}
ContentBlock::EmbeddedResource(res) => {
use rust_mcp_schema::EmbeddedResourceResource;
match &res.resource {
EmbeddedResourceResource::TextResourceContents(text) => {
write!(f, "[Embedded Text: {}]", text.uri)?;
}
EmbeddedResourceResource::BlobResourceContents(blob) => {
let mime = blob.mime_type.as_deref().unwrap_or("unknown");
write!(
f,
"[Embedded Blob: {}, {}, {} bytes]",
blob.uri,
mime,
blob.blob.len()
)?;
}
}
}
}
}
if let Some(structured) = &self.structured_content {
if !self.content.is_empty() {
writeln!(f)?;
writeln!(f)?;
}
writeln!(f, "Structured Content:")?;
let json = serde_json::to_string_pretty(structured).unwrap_or_default();
write!(f, "{}", json)?;
}
Ok(())
}
}
impl From<String> for ToolOutput {
fn from(text: String) -> Self {
Self::new().text(text)
}
}
impl From<&str> for ToolOutput {
fn from(text: &str) -> Self {
Self::new().text(text)
}
}
pub trait IntoToolResponse {
fn into_tool_response(self) -> CallToolResult;
}
impl IntoToolResponse for ToolOutput {
fn into_tool_response(self) -> CallToolResult {
self.into_call_result(false)
}
}
impl IntoToolResponse for String {
fn into_tool_response(self) -> CallToolResult {
ToolOutput::from(self).into_call_result(false)
}
}
impl IntoToolResponse for &str {
fn into_tool_response(self) -> CallToolResult {
ToolOutput::from(self).into_call_result(false)
}
}
impl<T, E> IntoToolResponse for Result<T, E>
where
T: Into<ToolOutput>,
E: std::fmt::Display,
{
fn into_tool_response(self) -> CallToolResult {
match self {
Ok(v) => v.into().into_call_result(false),
Err(e) => ToolOutput::new().text(e.to_string()).into_call_result(true),
}
}
}
#[derive(Debug)]
pub struct WithSource<E>(pub E);
impl<E: std::error::Error> fmt::Display for WithSource<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)?;
let mut current: &dyn std::error::Error = &self.0;
while let Some(source) = current.source() {
write!(f, ": {}", source)?;
current = source;
}
Ok(())
}
}
impl<E: std::error::Error> std::error::Error for WithSource<E> {}
impl<E: std::error::Error> From<E> for WithSource<E> {
fn from(err: E) -> Self {
Self(err)
}
}
#[derive(Debug)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: ToolInputSchema,
}
impl ToolDefinition {
pub fn from_tool<T: ToolDef>() -> Self {
let settings = schemars::r#gen::SchemaSettings::draft07().with(|s| {
s.option_add_null_type = false;
});
let schema = settings.into_generator().into_root_schema_for::<T>();
let json = serde_json::to_value(&schema).expect("schema serialization failed");
let input_schema = convert_schema_to_tool_input(&json);
Self {
name: T::NAME.to_string(),
description: T::DESCRIPTION.to_string(),
input_schema,
}
}
pub fn into_mcp_tool(self) -> rust_mcp_schema::Tool {
rust_mcp_schema::Tool {
name: self.name,
description: Some(self.description),
input_schema: self.input_schema,
annotations: None,
meta: None,
output_schema: None,
title: None,
}
}
}
impl fmt::Display for ToolDefinition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "## {}", self.name)?;
writeln!(f)?;
writeln!(f, "{}", self.description)?;
let Some(props) = &self.input_schema.properties else {
return Ok(());
};
if props.is_empty() {
return Ok(());
}
writeln!(f)?;
writeln!(f, "Parameters:")?;
let mut names: Vec<_> = props.keys().collect();
names.sort();
for name in names {
let prop = &props[name];
let required = self.input_schema.required.contains(name);
let req_str = if required { "required" } else { "optional" };
let type_str = prop.get("type").and_then(|v| v.as_str()).unwrap_or("any");
write!(f, " {name} ({type_str}, {req_str})")?;
if let Some(desc) = prop.get("description").and_then(|v| v.as_str()) {
writeln!(f)?;
write!(f, " {desc}")?;
}
if let Some(enum_vals) = prop.get("enum").and_then(|v| v.as_array()) {
let vals: Vec<_> = enum_vals.iter().filter_map(|v| v.as_str()).collect();
if !vals.is_empty() {
writeln!(f)?;
write!(f, " Values: {}", vals.join(", "))?;
}
}
writeln!(f)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct ToolDefinitions(Vec<ToolDefinition>);
impl ToolDefinitions {
pub fn new(definitions: Vec<ToolDefinition>) -> Self {
Self(definitions)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &ToolDefinition> {
self.0.iter()
}
}
impl Index<usize> for ToolDefinitions {
type Output = ToolDefinition;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl IntoIterator for ToolDefinitions {
type Item = ToolDefinition;
type IntoIter = std::vec::IntoIter<ToolDefinition>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a ToolDefinitions {
type Item = &'a ToolDefinition;
type IntoIter = std::slice::Iter<'a, ToolDefinition>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl fmt::Display for ToolDefinitions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "# Tools")?;
writeln!(f)?;
for (i, def) in self.0.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(f, "{def}")?;
}
Ok(())
}
}
fn convert_schema_to_tool_input(schema: &serde_json::Value) -> ToolInputSchema {
let required = schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let properties = schema
.get("properties")
.and_then(|p| p.as_object())
.map(|obj| {
obj.iter()
.map(|(k, v)| {
let map = v.as_object().cloned().unwrap_or_default();
(k.clone(), map)
})
.collect::<HashMap<_, _>>()
});
ToolInputSchema::new(required, properties)
}
pub trait ToolRegistry: Sized {
const ENABLED: bool = true;
fn parse(name: &str, arguments: serde_json::Value) -> std::result::Result<Self, JsonRpcError>;
fn definitions() -> ToolDefinitions;
}
#[derive(Debug)]
pub enum NoTools {}
impl ToolRegistry for NoTools {
const ENABLED: bool = false;
fn parse(name: &str, _arguments: serde_json::Value) -> std::result::Result<Self, JsonRpcError> {
Err(JsonRpcError::MethodNotFound {
msg: format!("unknown tool: {name}"),
})
}
fn definitions() -> ToolDefinitions {
ToolDefinitions::new(vec![])
}
}
impl<T: ToolDef> ToolRegistry for T {
fn parse(name: &str, arguments: serde_json::Value) -> std::result::Result<Self, JsonRpcError> {
if name == T::NAME {
serde_json::from_value(arguments).map_err(|e| JsonRpcError::InvalidParams {
msg: format!("{}: {e}", T::NAME),
})
} else {
Err(JsonRpcError::MethodNotFound {
msg: format!("unknown tool: {name}"),
})
}
}
fn definitions() -> ToolDefinitions {
ToolDefinitions::new(vec![ToolDefinition::from_tool::<T>()])
}
}
#[macro_export]
macro_rules! tool_registry {
(
enum $enum_name:ident {
$(
$variant:ident($tool_name:literal, $description:literal) {
$(
$(#[$field_meta:meta])*
$field_name:ident : $field_type:ty
),* $(,)?
}
),* $(,)?
}
) => {
$(
#[doc = concat!("Input parameters for the `", $tool_name, "` tool.")]
#[derive(Debug, $crate::schemars::JsonSchema, $crate::serde::Deserialize)]
#[schemars(crate = "::mercutio::schemars")]
#[serde(crate = "::mercutio::serde")]
pub struct $variant {
$(
$(#[$field_meta])*
pub $field_name: $field_type,
)*
}
impl $crate::ToolDef for $variant {
const NAME: &'static str = $tool_name;
const DESCRIPTION: &'static str = $description;
}
)*
#[doc = concat!("Tool dispatch enum for this server.")]
pub enum $enum_name {
$(
#[doc = concat!("The `", $tool_name, "` tool.")]
$variant($variant),
)*
}
impl $crate::ToolRegistry for $enum_name {
fn parse(
name: &str,
arguments: $crate::serde_json::Value,
) -> std::result::Result<Self, $crate::JsonRpcError> {
match name {
$(
$tool_name => {
let input: $variant = $crate::serde_json::from_value(arguments)
.map_err(|e| $crate::JsonRpcError::InvalidParams {
msg: format!("{}: {}", $tool_name, e),
})?;
Ok(Self::$variant(input))
}
)*
_ => Err($crate::JsonRpcError::MethodNotFound {
msg: format!("unknown tool: {name}"),
}),
}
}
fn definitions() -> $crate::ToolDefinitions {
$crate::ToolDefinitions::new(vec![
$(
$crate::ToolDefinition::from_tool::<$variant>(),
)*
])
}
}
};
}
#[cfg(test)]
mod tests {
use super::{IntoToolResponse, NoTools, ToolDefinition, ToolOutput, ToolRegistry};
use crate::JsonRpcError;
#[test]
fn no_tools_definitions_empty() {
assert!(NoTools::definitions().is_empty());
}
#[test]
fn no_tools_parse_returns_error() {
let result = NoTools::parse("anything", serde_json::Value::Null);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, JsonRpcError::MethodNotFound { .. }));
}
#[test]
fn tool_definition_from_tool() {
#[allow(dead_code)]
#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
struct TestInput {
value: String,
}
impl super::ToolDef for TestInput {
const NAME: &'static str = "test_tool";
const DESCRIPTION: &'static str = "A test tool";
}
let def = ToolDefinition::from_tool::<TestInput>();
assert_eq!(def.name, "test_tool");
assert_eq!(def.description, "A test tool");
assert_eq!(def.input_schema.type_(), "object");
assert!(def.input_schema.properties.is_some());
}
#[test]
fn field_docstrings_become_schema_descriptions() {
#[allow(dead_code)]
#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
struct TestInput {
city: String,
units: Option<String>,
}
impl super::ToolDef for TestInput {
const NAME: &'static str = "test";
const DESCRIPTION: &'static str = "Test";
}
let def = ToolDefinition::from_tool::<TestInput>();
let props = def.input_schema.properties.expect("properties");
let city_prop = props.get("city").expect("city property");
let city_desc = city_prop.get("description").and_then(|v| v.as_str());
assert_eq!(city_desc, Some("The city to look up."));
let units_prop = props.get("units").expect("units property");
let units_desc = units_prop.get("description").and_then(|v| v.as_str());
assert_eq!(units_desc, Some("Temperature unit preference."));
}
#[test]
fn tool_output_from_string() {
let result = "hello".into_tool_response();
let json = serde_json::to_value(&result).expect("serialize");
let content = json.get("content").expect("content field");
assert!(content.is_array());
assert_eq!(content.as_array().expect("array").len(), 1);
assert_eq!(json.get("isError").and_then(|v| v.as_bool()), Some(false));
}
#[test]
fn tool_output_from_owned_string() {
let result = String::from("hello").into_tool_response();
let json = serde_json::to_value(&result).expect("serialize");
let content = json.get("content").expect("content field");
assert!(content.is_array());
}
#[test]
fn tool_output_json_sets_structured_content() {
#[derive(serde::Serialize)]
struct Data {
value: i32,
}
let result = ToolOutput::json(&Data { value: 42 }).into_tool_response();
let json = serde_json::to_value(&result).expect("serialize");
assert!(json.get("structuredContent").is_some());
assert!(json.get("content").expect("content").is_array());
}
#[test]
fn result_err_sets_is_error() {
let result: Result<String, &str> = Err("something failed");
let json = serde_json::to_value(&result.into_tool_response()).expect("serialize");
assert_eq!(json.get("isError").and_then(|v| v.as_bool()), Some(true));
}
#[test]
fn result_ok_sets_is_error_false() {
let result: Result<&str, &str> = Ok("success");
let json = serde_json::to_value(&result.into_tool_response()).expect("serialize");
assert_eq!(json.get("isError").and_then(|v| v.as_bool()), Some(false));
}
#[test]
fn tool_output_builder_multiple_text_blocks() {
let result = ToolOutput::new()
.text("first")
.text("second")
.into_tool_response();
let json = serde_json::to_value(&result).expect("serialize");
let content = json.get("content").expect("content");
assert_eq!(content.as_array().expect("array").len(), 2);
}
#[test]
fn tool_output_builder_text_and_structured() {
#[derive(serde::Serialize)]
struct Data {
value: i32,
}
let result = ToolOutput::new()
.text("summary")
.structured(&Data { value: 1 })
.into_tool_response();
let json = serde_json::to_value(&result).expect("serialize");
assert!(json.get("structuredContent").is_some());
assert_eq!(
json.get("content")
.expect("content")
.as_array()
.expect("array")
.len(),
1
);
}
#[test]
fn tool_definition_display() {
#[allow(dead_code)]
#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
struct TestInput {
city: String,
units: Option<String>,
}
impl super::ToolDef for TestInput {
const NAME: &'static str = "get_weather";
const DESCRIPTION: &'static str = "Gets weather for a city";
}
let def = ToolDefinition::from_tool::<TestInput>();
insta::assert_snapshot!(def.to_string(), @r"
## get_weather
Gets weather for a city
Parameters:
city (string, required)
The city to look up.
units (string, optional)
Temperature unit preference.
");
}
#[test]
fn tool_definition_schema_json() {
#[allow(dead_code)]
#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
struct TestInput {
name: String,
count: Option<u32>,
}
impl super::ToolDef for TestInput {
const NAME: &'static str = "test";
const DESCRIPTION: &'static str = "Test tool";
}
let def = ToolDefinition::from_tool::<TestInput>();
let json = serde_json::to_value(&def.input_schema).expect("serialization failed");
insta::assert_snapshot!(serde_json::to_string_pretty(&json).expect("formatting failed"), @r#"
{
"properties": {
"count": {
"description": "Optional field.",
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"name": {
"description": "Required field.",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
"#);
}
#[test]
fn tool_definitions_display() {
#[allow(dead_code)]
#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
struct GetWeather {
location: String,
}
impl super::ToolDef for GetWeather {
const NAME: &'static str = "get_weather";
const DESCRIPTION: &'static str = "Gets weather for a location";
}
#[allow(dead_code)]
#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
struct SetReminder {
message: String,
delay_minutes: u32,
}
impl super::ToolDef for SetReminder {
const NAME: &'static str = "set_reminder";
const DESCRIPTION: &'static str = "Sets a reminder";
}
let defs = super::ToolDefinitions::new(vec![
ToolDefinition::from_tool::<GetWeather>(),
ToolDefinition::from_tool::<SetReminder>(),
]);
insta::assert_snapshot!(defs.to_string(), @r"
# Tools
## get_weather
Gets weather for a location
Parameters:
location (string, required)
City or address to look up.
## set_reminder
Sets a reminder
Parameters:
delay_minutes (integer, required)
Minutes from now.
message (string, required)
Reminder message.
");
}
#[test]
fn tool_output_display_text() {
let output = ToolOutput::new()
.text("Temperature: 72F")
.text("Conditions: Sunny");
insta::assert_snapshot!(output.to_string(), @r"
Temperature: 72F
Conditions: Sunny
");
}
#[test]
fn tool_output_display_with_structured() {
#[derive(serde::Serialize)]
struct Weather {
temp: i32,
conditions: String,
}
let output = ToolOutput::new()
.text("Current weather")
.structured(&Weather {
temp: 72,
conditions: "Sunny".into(),
});
insta::assert_snapshot!(output.to_string(), @r#"
Current weather
Structured Content:
{
"conditions": "Sunny",
"temp": 72
}
"#);
}
#[test]
fn tool_output_image_block() {
let png_data = b"\x89PNG\r\n\x1a\n";
let output = ToolOutput::new().image(png_data, "image/png");
insta::assert_snapshot!(output.to_string(), @"[Image: image/png, 12 bytes]");
}
#[test]
fn tool_output_audio_block() {
let wav_header = b"RIFF\x00\x00\x00\x00WAVEfmt ";
let output = ToolOutput::new().audio(wav_header, "audio/wav");
insta::assert_snapshot!(output.to_string(), @"[Audio: audio/wav, 24 bytes]");
}
#[test]
fn with_source_formats_error_chain() {
use std::fmt;
#[derive(Debug)]
struct OuterError(InnerError);
impl fmt::Display for OuterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "outer error")
}
}
impl std::error::Error for OuterError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
#[derive(Debug)]
struct InnerError;
impl fmt::Display for InnerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "inner cause")
}
}
impl std::error::Error for InnerError {}
let wrapped = super::WithSource(OuterError(InnerError));
assert_eq!(wrapped.to_string(), "outer error: inner cause");
}
}