#![allow(dead_code)]
use proc_macro2::{Span, TokenStream};
use quote::quote_spanned;
const SERVER_ATTRS: &[&str] = &["name", "version", "instructions", "debug_expand"];
const TOOL_ATTRS: &[&str] = &[
"description",
"name",
"destructive",
"idempotent",
"read_only",
];
const RESOURCE_ATTRS: &[&str] = &["uri_pattern", "name", "description", "mime_type"];
const PROMPT_ATTRS: &[&str] = &["description", "name"];
pub fn unknown_attr_error(attr_name: &str, context: AttrContext, span: Span) -> TokenStream {
let known = match context {
AttrContext::Server => SERVER_ATTRS,
AttrContext::Tool => TOOL_ATTRS,
AttrContext::Resource => RESOURCE_ATTRS,
AttrContext::Prompt => PROMPT_ATTRS,
};
let suggestion = find_similar(attr_name, known);
let known_list = known.join(", ");
let message = if let Some(similar) = suggestion {
format!(
"unknown attribute `{attr_name}`\n\n\
help: did you mean `{similar}`?\n\
note: valid attributes are: {known_list}"
)
} else {
format!(
"unknown attribute `{attr_name}`\n\n\
note: valid attributes are: {known_list}"
)
};
quote_spanned!(span => compile_error!(#message);)
}
pub fn missing_attr_error(attr_name: &str, context: AttrContext, span: Span) -> TokenStream {
let context_name = match context {
AttrContext::Server => "mcp_server",
AttrContext::Tool => "tool",
AttrContext::Resource => "resource",
AttrContext::Prompt => "prompt",
};
let message = format!(
"missing required attribute `{attr_name}` for #[{context_name}]\n\n\
help: add `{attr_name} = \"...\"` to the attribute"
);
quote_spanned!(span => compile_error!(#message);)
}
pub fn invalid_value_error(attr_name: &str, expected: &str, got: &str, span: Span) -> TokenStream {
let message = format!("invalid value for `{attr_name}`: expected {expected}, got `{got}`");
quote_spanned!(span => compile_error!(#message);)
}
pub fn tool_outside_server_error(span: Span) -> TokenStream {
let message = "\
#[tool] must be used inside an #[mcp_server] impl block\n\n\
help: wrap your impl block with #[mcp_server(name = \"...\", version = \"...\")]";
quote_spanned!(span => compile_error!(#message);)
}
pub fn invalid_signature_error(issue: &str, span: Span) -> TokenStream {
let message = format!("invalid method signature: {issue}");
quote_spanned!(span => compile_error!(#message);)
}
#[derive(Debug, Clone, Copy)]
pub enum AttrContext {
Server,
Tool,
Resource,
Prompt,
}
fn find_similar<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
let input_lower = input.to_lowercase();
for candidate in candidates {
if candidate.starts_with(&input_lower) || input_lower.starts_with(candidate) {
return Some(candidate);
}
}
candidates
.iter()
.filter_map(|candidate| {
let dist = levenshtein(&input_lower, candidate);
if dist <= 2 {
Some((*candidate, dist))
} else {
None
}
})
.min_by_key(|(_, dist)| *dist)
.map(|(candidate, _)| candidate)
}
fn levenshtein(a: &str, b: &str) -> usize {
let a_len = a.chars().count();
let b_len = b.chars().count();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut matrix = vec![vec![0usize; b_len + 1]; a_len + 1];
for (i, row) in matrix.iter_mut().enumerate() {
row[0] = i;
}
for (j, cell) in matrix[0].iter_mut().enumerate() {
*cell = j;
}
for (i, a_char) in a.chars().enumerate() {
for (j, b_char) in b.chars().enumerate() {
let cost = usize::from(a_char != b_char);
matrix[i + 1][j + 1] = (matrix[i][j + 1] + 1)
.min(matrix[i + 1][j] + 1)
.min(matrix[i][j] + cost);
}
}
matrix[a_len][b_len]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_similar() {
assert_eq!(find_similar("descripion", TOOL_ATTRS), Some("description"));
assert_eq!(find_similar("nam", TOOL_ATTRS), Some("name"));
assert_eq!(find_similar("xyz123", TOOL_ATTRS), None);
}
#[test]
fn test_levenshtein() {
assert_eq!(levenshtein("", ""), 0);
assert_eq!(levenshtein("abc", "abc"), 0);
assert_eq!(levenshtein("abc", "abd"), 1);
assert_eq!(levenshtein("abc", "abcd"), 1);
assert_eq!(levenshtein("kitten", "sitting"), 3);
}
}