fix(tasks): expose execution.taskSupport on tools (#635)

* fix(tasks): expose execution.taskSupport on tools

* feat: implement taskSupport validation on server
This commit is contained in:
Luca Chang 2026-02-03 16:17:36 -08:00 • committed by GitHub
commit df6c3f0665
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 539 additions and 2 deletions

View file

@ -89,12 +89,21 @@ pub struct ToolAttribute {
pub output_schema: Option<Expr>,
/// Optional additional tool information.
pub annotations: Option<ToolAnnotationsAttribute>,
/// Execution-related configuration including task support.
pub execution: Option<ToolExecutionAttribute>,
/// Optional icons for the tool
pub icons: Option<Expr>,
/// Optional metadata for the tool
pub meta: Option<Expr>,
}
#[derive(FromMeta, Debug, Default)]
#[darling(default)]
pub struct ToolExecutionAttribute {
/// Task support mode: "forbidden", "optional", or "required"
pub task_support: Option<String>,
}
pub struct ResolvedToolAttribute {
pub name: String,
pub title: Option<String>,
@ -102,6 +111,7 @@ pub struct ResolvedToolAttribute {
pub input_schema: Expr,
pub output_schema: Option<Expr>,
pub annotations: Expr,
pub execution: Expr,
pub icons: Option<Expr>,
pub meta: Option<Expr>,
}
@ -115,6 +125,7 @@ impl ResolvedToolAttribute {
input_schema,
output_schema,
annotations,
execution,
icons,
meta,
} = self;
@ -155,6 +166,7 @@ impl ResolvedToolAttribute {
input_schema: #input_schema,
output_schema: #output_schema,
annotations: #annotations,
execution: #execution,
icons: #icons,
meta: #meta,
}
@ -263,6 +275,38 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
} else {
none_expr()?
};
let execution_expr = if let Some(execution) = attribute.execution {
let ToolExecutionAttribute { task_support } = execution;
let task_support_expr = if let Some(ts) = task_support {
let ts_ident = match ts.as_str() {
"forbidden" => quote! { rmcp::model::TaskSupport::Forbidden },
"optional" => quote! { rmcp::model::TaskSupport::Optional },
"required" => quote! { rmcp::model::TaskSupport::Required },
_ => {
return Err(syn::Error::new(
Span::call_site(),
format!(
"Invalid task_support value '{}'. Expected 'forbidden', 'optional', or 'required'",
ts
),
));
}
};
quote! { Some(#ts_ident) }
} else {
quote! { None }
};
let token_stream = quote! {
Some(rmcp::model::ToolExecution {
task_support: #task_support_expr,
})
};
syn::parse2::<Expr>(token_stream)?
} else {
none_expr()?
};
// Handle output_schema - either explicit or generated from return type
let output_schema_expr = attribute.output_schema.or_else(|| {
// Try to generate schema from return type
@ -286,6 +330,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
input_schema: input_schema_expr,
output_schema: output_schema_expr,
annotations: annotations_expr,
execution: execution_expr,
title: attribute.title,
icons: attribute.icons,
meta: attribute.meta,

View file

@ -56,9 +56,18 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
})
}
};
let get_tool_fn = quote! {
fn get_tool(&self, name: &str) -> Option<rmcp::model::Tool> {
#router.get(name).cloned()
}
};
let tool_call_fn = syn::parse2::<ImplItem>(tool_call_fn)?;
let tool_list_fn = syn::parse2::<ImplItem>(tool_list_fn)?;
let get_tool_fn = syn::parse2::<ImplItem>(get_tool_fn)?;
item_impl.items.push(tool_call_fn);
item_impl.items.push(tool_list_fn);
item_impl.items.push(get_tool_fn);
Ok(item_impl.into_token_stream())
}