1use proc_macro::TokenStream;
46use proc_macro2::TokenStream as TokenStream2;
47use quote::{format_ident, quote};
48use syn::{
49 FnArg, GenericArgument, ImplItem, ImplItemFn, ItemImpl, PathArguments, ReturnType, Type,
50 TypePath, parse_macro_input,
51};
52
53#[proc_macro_attribute]
54pub fn mcp_expose(_attr: TokenStream, item: TokenStream) -> TokenStream {
55 let input = parse_macro_input!(item as ItemImpl);
56
57 let self_ty = &input.self_ty;
59 let self_ident = extract_type_ident(self_ty).unwrap_or_else(|| format_ident!("Impl"));
60 let adapter_ident = format_ident!("{}McpAdapter", self_ident);
61
62 let mut tool_methods = Vec::new();
64 let mut skipped = Vec::new();
65 for item in &input.items {
66 if let ImplItem::Fn(method) = item {
67 match parse_grpc_method(method) {
68 Some(spec) => tool_methods.push(emit_adapter_tool(self_ty, &spec)),
69 None => skipped.push(method.sig.ident.to_string()),
70 }
71 }
72 }
73 if tool_methods.is_empty() {
74 let msg = format!(
75 "mcp_expose: no gRPC-shaped methods found. Methods seen: [{}]. \
76 Expected each to be `async fn name(&self, req: Request<X>) -> Result<Response<Y>, Status>`.",
77 skipped.join(", ")
78 );
79 return syn::Error::new_spanned(self_ty, msg)
80 .to_compile_error()
81 .into();
82 }
83
84 let adapter = quote! {
91 use ::rmcp::{tool, tool_router, tool_handler, ServerHandler as __ToninMcpServerHandler};
95
96 #[derive(::std::clone::Clone)]
97 pub struct #adapter_ident {
98 inner: ::std::sync::Arc<#self_ty>,
99 tool_router: ::rmcp::handler::server::router::tool::ToolRouter<#adapter_ident>,
100 }
101
102 impl #adapter_ident {
103 pub fn new(inner: #self_ty) -> Self {
104 Self {
105 inner: ::std::sync::Arc::new(inner),
106 tool_router: Self::tool_router(),
107 }
108 }
109 }
110
111 #[tool_router]
112 impl #adapter_ident {
113 #(#tool_methods)*
114 }
115
116 #[tool_handler]
117 impl __ToninMcpServerHandler for #adapter_ident {
118 fn get_info(&self) -> ::rmcp::model::ServerInfo {
119 ::rmcp::model::ServerInfo::new(
120 ::rmcp::model::ServerCapabilities::builder().enable_tools().build(),
121 )
122 .with_server_info(::rmcp::model::Implementation::from_build_env())
123 .with_protocol_version(::rmcp::model::ProtocolVersion::V_2024_11_05)
124 .with_instructions(
125 "tonin service MCP endpoint. Tools auto-derived from gRPC methods.".to_string(),
126 )
127 }
128 }
129 };
130
131 let expanded = quote! {
132 #input
133 #adapter
134 };
135
136 expanded.into()
137}
138
139struct GrpcMethodSpec {
140 rust_name: syn::Ident,
141 description: String,
142 request_type: Type,
143}
144
145fn parse_grpc_method(method: &ImplItemFn) -> Option<GrpcMethodSpec> {
146 let sig = &method.sig;
147 sig.asyncness.as_ref()?;
148 let mut inputs = sig.inputs.iter();
149 match inputs.next() {
150 Some(FnArg::Receiver(_)) => {}
151 _ => return None,
152 }
153 let second = inputs.next()?;
154 let pat_type = match second {
155 FnArg::Typed(t) => t,
156 _ => return None,
157 };
158 let request_type = extract_request_inner(&pat_type.ty)?;
159 if !matches!(&sig.output, ReturnType::Type(_, _)) {
164 return None;
165 }
166 let rust_name = sig.ident.clone();
167 let description =
168 extract_doc_comment(method).unwrap_or_else(|| format!("gRPC method {rust_name}"));
169 Some(GrpcMethodSpec {
170 rust_name,
171 description,
172 request_type,
173 })
174}
175
176fn extract_type_ident(ty: &Type) -> Option<syn::Ident> {
177 if let Type::Path(TypePath { path, .. }) = ty {
178 return path.segments.last().map(|s| s.ident.clone());
179 }
180 None
181}
182
183fn extract_request_inner(ty: &Type) -> Option<Type> {
184 let path = match ty {
185 Type::Path(TypePath { path, .. }) => path,
186 _ => return None,
187 };
188 let last = path.segments.last()?;
189 if last.ident != "Request" {
190 return None;
191 }
192 let args = match &last.arguments {
193 PathArguments::AngleBracketed(a) => a,
194 _ => return None,
195 };
196 for arg in &args.args {
197 if let GenericArgument::Type(t) = arg {
198 return Some(t.clone());
199 }
200 }
201 None
202}
203
204fn extract_doc_comment(method: &ImplItemFn) -> Option<String> {
205 for attr in &method.attrs {
206 if attr.path().is_ident("doc")
207 && let syn::Meta::NameValue(nv) = &attr.meta
208 && let syn::Expr::Lit(syn::ExprLit {
209 lit: syn::Lit::Str(s),
210 ..
211 }) = &nv.value
212 {
213 return Some(s.value().trim().to_string());
214 }
215 }
216 None
217}
218
219fn emit_adapter_tool(self_ty: &Type, spec: &GrpcMethodSpec) -> TokenStream2 {
225 let method = &spec.rust_name;
226 let description = &spec.description;
227 let req_ty = &spec.request_type;
228 quote! {
229 #[tool(description = #description)]
230 async fn #method(
231 &self,
232 ::rmcp::handler::server::wrapper::Parameters(req): ::rmcp::handler::server::wrapper::Parameters<#req_ty>,
233 ) -> ::std::result::Result<::rmcp::model::CallToolResult, ::rmcp::ErrorData> {
234 let inner: ::std::sync::Arc<#self_ty> = ::std::sync::Arc::clone(&self.inner);
235 let tonic_req = ::tonic::Request::new(req);
236 match inner.#method(tonic_req).await {
237 Ok(resp) => {
238 let body = resp.into_inner();
239 let body_json = ::serde_json::to_string(&body)
240 .map_err(|e| ::rmcp::ErrorData::internal_error(
241 format!("serialize response: {}", e),
242 ::std::option::Option::None,
243 ))?;
244 Ok(::rmcp::model::CallToolResult::success(vec![
245 ::rmcp::model::Content::text(body_json),
246 ]))
247 }
248 Err(status) => Err(::rmcp::ErrorData::internal_error(
249 status.message().to_string(),
250 ::std::option::Option::None,
251 )),
252 }
253 }
254 }
255}