Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 147 additions & 37 deletions crates/rmcp-macros/src/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,10 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility,Lit,
parse::{Parse, discouraged::Speculative},
parse_quote,
spanned::Spanned,
};

/// Stores tool annotation attributes
Expand DownExpand Up@@ -42,13 +44,17 @@ impl Parse for ToolAnnotationAttrs {
}

#[derive(Default)]
struct ToolImplItemAttrs {
pub(crate) struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
default_build: bool,
description: Option<Expr>,
}

impl Parse for ToolImplItemAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tool_box = None;
let mut default = true;
let mut description = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
match key.to_string().as_str() {
Expand All@@ -60,6 +66,32 @@ impl Parse for ToolImplItemAttrs {
tool_box = Some(Some(value));
}
}
"default_build" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
match value.to_token_stream().to_string().as_str() {
"true" => {
default = true;
}
"false" => {
default = false;
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
}
} else {
default = true;
}
}
"description" => {
if input.lookahead1().peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
description = Some(value);
}
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
Expand All@@ -70,7 +102,11 @@ impl Parse for ToolImplItemAttrs {
input.parse::<Token![,]>()?;
}

Ok(ToolImplItemAttrs { tool_box })
Ok(ToolImplItemAttrs {
tool_box,
default_build: default,
description,
})
}
}

Expand All@@ -79,6 +115,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
aggr: bool,
annotations: Option<ToolAnnotationAttrs>,
}

Expand All@@ -87,12 +124,18 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut aggr = false;
let mut annotations = None;

while !input.is_empty() {
let key: Ident = input.parse()?;
let key_str = key.to_string();
if key_str == AGGREGATED_IDENT {
aggr = true;
continue;
}
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
match key_str.as_str() {
"name" => {
let value: Expr = input.parse()?;
name = Some(value);
Expand DownExpand Up@@ -126,6 +169,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
aggr,
annotations,
})
}
Expand DownExpand Up@@ -200,14 +244,20 @@ pub enum ToolItem {

impl Parse for ToolItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![impl]) {
let item = input.parse::<ItemImpl>()?;
Ok(ToolItem::Impl(item))
} else {
let item = input.parse::<ItemFn>()?;
Ok(ToolItem::Fn(item))
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemImpl>() {
input.advance_to(&fork);
return Ok(ToolItem::Impl(item));
}
let fork = input.fork();
if let Ok(item) = fork.parse::<ItemFn>() {
input.advance_to(&fork);
return Ok(ToolItem::Fn(item));
}
Err(syn::Error::new(
input.span(),
"expected function or impl block",
))
}
}

Expand All@@ -223,7 +273,22 @@ pub(crate) fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Result<TokenStream> {
let tool_impl_attr: ToolImplItemAttrs = syn::parse2(attr)?;
let tool_box_ident = tool_impl_attr.tool_box;

let mut extend_quote = None;
let description = if let Some(expr) = tool_impl_attr.description {
// Use explicitly provided description if available
expr
} else {
// Try to extract documentation comments
let doc_content = input
.attrs
.iter()
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");
parse_quote! {
#doc_content.trim().to_string()
}
};
// get all tool function ident
let mut tool_fn_idents = Vec::new();
for item in &input.items {
Expand DownExpand Up@@ -325,6 +390,37 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
})
}
});

if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
self.call_tool_inner(request, context).await
}
async fn list_tools(
&self,
request: Option<rmcp::model::PaginatedRequestParam>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::Error> {
self.list_tools_inner(request.unwrap_or_default(), context).await
}
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
} else {
// if there are no generic parameters, use the original tool_box! macro
let this_type_ident = &input.self_ty;
Expand All@@ -333,11 +429,30 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res
#(#tool_fn_idents),*
} #ident);
));
if tool_impl_attr.default_build {
let struct_name = input.self_ty.clone();
let generic = &input.generics;
let extend = quote! {
impl #generic rmcp::handler::server::ServerHandler for #struct_name {
rmcp::tool_box!(@derive #ident);

fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo {
instructions: Some(#description.into()),
capabilities: rmcp::model::ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
};
extend_quote.replace(extend);
}
}
}

Ok(quote! {
#input
#extend_quote
})
}

Expand DownExpand Up@@ -391,29 +506,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
for attr in raw_attrs {
match &attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident(TOOL_IDENT) {
let pat_type = pat_type.clone();
let marker = meta_list.parse_args::<ParamMarker>()?;
match marker {
ParamMarker::Param => {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
ParamMarker::Aggregated => {
caught.replace(Caught::Aggregated(pat_type.clone()));
}
}
} else if meta_list.path.is_ident(SERDE_IDENT) {
if meta_list.path.is_ident(SERDE_IDENT) {
serde_metas.push(meta_list.clone());
} else if meta_list.path.is_ident(SCHEMARS_IDENT) {
schemars_metas.push(meta_list.clone());
Expand All@@ -426,6 +519,23 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
}
}
}
let pat_type = pat_type.clone();
if tool_macro_attrs.fn_item.aggr {
caught.replace(Caught::Aggregated(pat_type.clone()));
} else {
let Some(arg_ident) = arg_ident.take() else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"input param must have an ident as name",
));
};
caught.replace(Caught::Param(ToolFnParamAttrs {
serde_meta: Vec::new(),
schemars_meta: Vec::new(),
ident: arg_ident,
rust_type: pat_type.ty.clone(),
}));
}
match caught {
Some(Caught::Param(mut param)) => {
param.serde_meta = serde_metas;
Expand DownExpand Up@@ -483,7 +593,6 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
.filter_map(extract_doc_line)
.collect::<Vec<_>>()
.join("\n");

parse_quote! {
#doc_content.trim().to_string()
}
Expand DownExpand Up@@ -759,6 +868,7 @@ mod test {

// The output should contain the description from doc comments
let result_str = result.to_string();
println!("result: {:#}", result_str);
assert!(result_str.contains("This is a test description from doc comments"));
assert!(result_str.contains("with multiple lines"));

Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }

# macro
rmcp-macros = { version = "0.1", workspace = true, optional = true }
rmcp-macros = { workspace = true, optional = true }

[features]
default = ["base64", "macros", "server"]
Expand Down
31 changes: 6 additions & 25 deletions crates/rmcp/tests/common/calculator.rs
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
use rmcp::{
ServerHandler,
model::{ServerCapabilities, ServerInfo},
schemars, tool,
};
use rmcp::{schemars, tool};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest {
#[schemars(description = "the left hand side number")]
Expand All@@ -11,34 +7,19 @@ pub struct SumRequest {
}
#[derive(Debug, Clone, Default)]
pub struct Calculator;
#[tool(tool_box)]
#[tool(tool_box, description = "A simple calculator")]
impl Calculator {
#[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
#[tool(description = "Calculate the sum of two numbers", aggr)]
fn sum(&self, SumRequest { a, b }: SumRequest) -> String {
(a + b).to_string()
}

#[tool(description = "Calculate the sub of two numbers")]
fn sub(
&self,
#[tool(param)]
#[schemars(description = "the left hand side number")]
a: i32,
#[tool(param)]
#[schemars(description = "the right hand side number")]
b: i32,
#[schemars(description = "the left hand side number")] a: i32,
#[schemars(description = "the right hand side number")] b: i32,
) -> String {
(a - b).to_string()
}
}

#[tool(tool_box)]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("A simple calculator".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
7 changes: 2 additions & 5 deletions crates/rmcp/tests/test_complex_schema.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,11 +30,8 @@ impl Demo {
Self
}

#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
#[tool(description = "LLM", aggr)]
async fn chat(&self, chat_request: ChatRequest) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/tests/test_tool_macros.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
pub async fn get_weather(&self, city: String) -> String {
drop(city);
"rain".to_string()
}
Expand Down
Loading