#![allow(dead_code)]
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{FnArg, Ident, Pat, PatIdent, PatType, ReturnType, Type};
#[derive(Debug)]
pub struct ToolMethod {
pub name: Ident,
pub tool_name: String,
pub description: String,
pub destructive: bool,
pub idempotent: bool,
pub read_only: bool,
pub params: Vec<ToolParam>,
pub is_async: bool,
pub returns_result: bool,
}
#[derive(Debug)]
pub struct ToolParam {
pub name: Ident,
pub ty: Type,
pub doc: Option<String>,
pub is_optional: bool,
pub default: Option<syn::Lit>,
}
impl ToolMethod {
pub fn generate_input_schema(&self) -> TokenStream {
let mut properties = Vec::new();
let mut required = Vec::new();
for param in &self.params {
let name = param.name.to_string();
let ty = ¶m.ty;
let type_schema = type_to_json_schema(ty);
let description = param.doc.as_ref().map_or_else(
|| quote!(::serde_json::Value::Null),
|d| quote!(::serde_json::Value::String(#d.to_string())),
);
properties.push(quote! {
(#name.to_string(), {
let mut prop = #type_schema;
if let ::serde_json::Value::Object(ref mut obj) = prop {
if #description != ::serde_json::Value::Null {
obj.insert("description".to_string(), #description);
}
}
prop
})
});
if !param.is_optional {
required.push(name);
}
}
quote! {
{
let mut schema = ::serde_json::json!({
"type": "object",
"properties": {},
});
if let ::serde_json::Value::Object(ref mut obj) = schema {
let properties: std::collections::HashMap<String, ::serde_json::Value> =
vec![#(#properties),*].into_iter().collect();
obj.insert("properties".to_string(), ::serde_json::Value::Object(
properties.into_iter().collect()
));
let required: Vec<String> = vec![#(#required.to_string()),*];
if !required.is_empty() {
obj.insert("required".to_string(), ::serde_json::Value::Array(
required.into_iter().map(::serde_json::Value::String).collect()
));
}
}
schema
}
}
}
pub fn generate_call_dispatch(&self) -> TokenStream {
let method_name = &self.name;
let tool_name = &self.tool_name;
let param_extractions: Vec<_> = self
.params
.iter()
.map(|param| {
let name = ¶m.name;
let name_str = name.to_string();
let ty = ¶m.ty;
if param.is_optional {
quote! {
let #name: #ty = args.get(#name_str)
.and_then(|v| ::serde_json::from_value(v.clone()).ok());
}
} else {
let value_var = quote::format_ident!("__{}_value", name);
quote! {
let #value_var = args.get(#name_str)
.ok_or_else(|| ::mcpkit::error::McpError::invalid_params(
#tool_name,
format!("missing required parameter: {}", #name_str),
))?
.clone();
let #name: #ty = ::serde_json::from_value(#value_var)
.map_err(|e| ::mcpkit::error::McpError::invalid_params(
#tool_name,
format!("invalid parameter '{}': {}", #name_str, e),
))?;
}
}
})
.collect();
let param_names: Vec<_> = self.params.iter().map(|p| &p.name).collect();
let call = if self.is_async {
quote!(self.#method_name(#(#param_names),*).await)
} else {
quote!(self.#method_name(#(#param_names),*))
};
let call_with_conversion = if self.returns_result {
quote!(#call)
} else {
quote!(Ok(#call))
};
quote! {
#tool_name => {
#(#param_extractions)*
#call_with_conversion
}
}
}
}
fn type_to_json_schema(ty: &Type) -> TokenStream {
if let Type::Path(path) = ty {
let path_str = quote!(#path).to_string().replace(' ', "");
match path_str.as_str() {
"String" | "&str" | "str" => quote!(::serde_json::json!({"type": "string"})),
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64"
| "u128" | "usize" => {
quote!(::serde_json::json!({"type": "integer"}))
}
"f32" | "f64" => quote!(::serde_json::json!({"type": "number"})),
"bool" => quote!(::serde_json::json!({"type": "boolean"})),
_ if path_str.starts_with("Option<") => {
if let Some(segment) = path.path.segments.last() {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
let inner_schema = type_to_json_schema(inner);
return quote! {
{
let mut schema = #inner_schema;
schema
}
};
}
}
}
quote!(::serde_json::json!({}))
}
_ if path_str.starts_with("Vec<") => {
if let Some(segment) = path.path.segments.last() {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
let inner_schema = type_to_json_schema(inner);
return quote! {
::serde_json::json!({
"type": "array",
"items": #inner_schema
})
};
}
}
}
quote!(::serde_json::json!({"type": "array"}))
}
_ if path_str.starts_with("HashMap<") || path_str.starts_with("BTreeMap<") => {
quote!(::serde_json::json!({
"type": "object",
"additionalProperties": true
}))
}
"serde_json::Value" | "Value" => {
quote!(::serde_json::json!({}))
}
_ => {
quote!(#path::tool_input_schema())
}
}
} else {
quote!(::serde_json::json!({}))
}
}
pub fn extract_param(arg: &FnArg) -> Option<ToolParam> {
match arg {
FnArg::Typed(PatType { pat, ty, attrs, .. }) => {
let name = match pat.as_ref() {
Pat::Ident(PatIdent { ident, .. }) => ident.clone(),
_ => return None,
};
let doc = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc") {
if let syn::Meta::NameValue(nv) = &attr.meta {
if let syn::Expr::Lit(lit) = &nv.value {
if let syn::Lit::Str(s) = &lit.lit {
return Some(s.value().trim().to_string());
}
}
}
}
None
})
.collect::<Vec<_>>()
.join(" ");
let doc = if doc.is_empty() { None } else { Some(doc) };
let is_optional = is_option_type(ty);
Some(ToolParam {
name,
ty: (**ty).clone(),
doc,
is_optional,
default: None,
})
}
FnArg::Receiver(_) => None,
}
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(path) = ty {
if let Some(segment) = path.path.segments.last() {
return segment.ident == "Option";
}
}
false
}
pub fn is_result_type(ret: &ReturnType) -> bool {
match ret {
ReturnType::Type(_, ty) => {
if let Type::Path(path) = ty.as_ref() {
if let Some(segment) = path.path.segments.last() {
return segment.ident == "Result";
}
}
false
}
ReturnType::Default => false,
}
}
pub fn gen_ident(base: &str, suffix: usize) -> Ident {
format_ident!("{}_{}", base, suffix)
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_is_option_type() {
let ty: Type = parse_quote!(Option<String>);
assert!(is_option_type(&ty));
let ty: Type = parse_quote!(String);
assert!(!is_option_type(&ty));
}
#[test]
fn test_is_result_type() {
let ret: ReturnType = parse_quote!(-> Result<ToolOutput, McpError>);
assert!(is_result_type(&ret));
let ret: ReturnType = parse_quote!(-> ToolOutput);
assert!(!is_result_type(&ret));
}
}