Skip to main content

mcp_plugin_api/
macros.rs

1//! Macros for simplifying plugin declaration
2
3/// Declare tools and auto-generate list_tools and execute_tool functions
4///
5/// This macro takes a list of Tool definitions and generates:
6/// - A static tool registry (HashMap for O(1) lookup)
7/// - The `generated_list_tools` function
8/// - The `generated_execute_tool` function
9///
10/// These generated functions can be used directly in the `declare_plugin!` macro.
11///
12/// # Example
13///
14/// ```ignore
15/// use mcp_plugin_api::*;
16/// use serde_json::{json, Value};
17///
18/// fn handle_hello(args: &Value) -> Result<Value, String> {
19///     let name = args["name"].as_str().unwrap_or("World");
20///     Ok(json!({ "message": format!("Hello, {}!", name) }))
21/// }
22///
23/// fn handle_goodbye(args: &Value) -> Result<Value, String> {
24///     Ok(json!({ "message": "Goodbye!" }))
25/// }
26///
27/// declare_tools! {
28///     tools: [
29///         Tool::new("hello", "Say hello")
30///             .param_string("name", "Name to greet", false)
31///             .handler(handle_hello),
32///         
33///         Tool::new("goodbye", "Say goodbye")
34///             .handler(handle_goodbye),
35///     ]
36/// }
37///
38/// declare_plugin! {
39///     list_tools: generated_list_tools,
40///     execute_tool: generated_execute_tool,
41///     free_string: mcp_plugin_api::utils::standard_free_string
42/// }
43/// ```
44#[macro_export]
45macro_rules! declare_tools {
46    (tools: [ $($tool:expr),* $(,)? ]) => {
47        static TOOLS: ::std::sync::OnceLock<::std::collections::HashMap<::std::string::String, $crate::tool::Tool>>
48            = ::std::sync::OnceLock::new();
49        static TOOLS_LIST_CACHE: ::std::sync::OnceLock<::std::vec::Vec<u8>>
50            = ::std::sync::OnceLock::new();
51
52        fn get_tools() -> &'static ::std::collections::HashMap<::std::string::String, $crate::tool::Tool> {
53            TOOLS.get_or_init(|| {
54                let mut map = ::std::collections::HashMap::new();
55                $(
56                    let tool = $tool;
57                    map.insert(tool.name.clone(), tool);
58                )*
59                map
60            })
61        }
62
63        fn get_tools_list_cache() -> &'static [u8] {
64            TOOLS_LIST_CACHE.get_or_init(|| {
65                let tools = get_tools();
66                let tools_json: ::std::vec::Vec<$crate::serde_json::Value> = tools
67                    .values()
68                    .filter(|t| t.active)
69                    .map(|t| t.to_json_schema())
70                    .collect();
71                let json_array = $crate::serde_json::Value::Array(tools_json);
72                json_array.to_string().into_bytes()
73            })
74        }
75
76        /// Auto-generated list_tools — returns pre-serialized JSON (one memcpy).
77        #[no_mangle]
78        pub unsafe extern "C" fn generated_list_tools(
79            result_buf: *mut *mut u8,
80            result_len: *mut usize,
81        ) -> i32 {
82            $crate::utils::return_prebuilt(get_tools_list_cache(), result_buf, result_len)
83        }
84
85        /// Auto-generated execute_tool — O(1) HashMap lookup then handler dispatch.
86        #[no_mangle]
87        pub unsafe extern "C" fn generated_execute_tool(
88            tool_name: *const ::std::os::raw::c_char,
89            args_json: *const u8,
90            args_len: usize,
91            result_buf: *mut *mut u8,
92            result_len: *mut usize,
93        ) -> i32 {
94            use ::std::ffi::CStr;
95
96            let name = match CStr::from_ptr(tool_name).to_str() {
97                Ok(s) => s,
98                Err(_) => return $crate::utils::return_error(
99                    "Invalid tool name encoding",
100                    result_buf,
101                    result_len
102                ),
103            };
104
105            let args_slice = ::std::slice::from_raw_parts(args_json, args_len);
106            let args: $crate::serde_json::Value = match $crate::serde_json::from_slice(args_slice) {
107                Ok(v) => v,
108                Err(e) => return $crate::utils::return_error(
109                    &format!("Invalid JSON arguments: {}", e),
110                    result_buf,
111                    result_len
112                ),
113            };
114
115            let tools = get_tools();
116            match tools.get(name) {
117                Some(tool) => {
118                    if tool.active {
119                        match (tool.handler)(&args) {
120                            Ok(result) => $crate::utils::return_success(
121                                result,
122                                result_buf,
123                                result_len
124                            ),
125                            Err(e) => $crate::utils::return_error(
126                                &e,
127                                result_buf,
128                                result_len
129                            ),
130                        }
131                    } else {
132                        $crate::utils::return_error(
133                            &format!("Inactive tool: {}", name),
134                            result_buf,
135                            result_len)
136                    }
137                }
138                None => $crate::utils::return_error(
139                    &format!("Unknown tool: {}", name),
140                    result_buf,
141                    result_len
142                ),
143            }
144        }
145    };
146}
147
148/// Declare resources and auto-generate list_resources, list_resource_templates, and read_resource
149///
150/// Dispatches `read_resource` in order: exact static URI, first matching URI template
151/// (`{var}` placeholders), then optional `read_fallback`.
152///
153/// # Example (static resources only)
154///
155/// ```ignore
156/// declare_resources! {
157///     resources: [
158///         Resource::builder("file:///docs/readme", read_readme)
159///             .name("readme.md")
160///             .build(),
161///     ]
162/// }
163/// ```
164///
165/// # Example (templates + fallback)
166///
167/// ```ignore
168/// fn read_file(uri: &str, vars: &HashMap<String, String>) -> Result<ResourceContents, String> {
169///     let path = vars.get("path").ok_or("missing path")?;
170///     Ok(vec![ResourceContent::text(uri, contents, Some("text/plain".into()))])
171/// }
172///
173/// fn read_any(uri: &str) -> Result<ResourceContents, String> {
174///     Err("not found".into())
175/// }
176///
177/// declare_resources! {
178///     resources: [],
179///     templates: [
180///         ResourceTemplate::builder("file:///project/{path}", read_file)
181///             .name("project-files")
182///             .build(),
183///     ],
184///     read_fallback: read_any
185/// }
186/// ```
187#[macro_export]
188macro_rules! declare_resources {
189    (resources: [ $($resource:expr),* $(,)? ]) => {
190        $crate::declare_resources!(@impl
191            resources: [$($resource),*],
192            templates: [],
193            read_fallback: []
194        );
195    };
196    (
197        resources: [ $($resource:expr),* $(,)? ],
198        templates: [ $($template:expr),* $(,)? ]
199    ) => {
200        $crate::declare_resources!(@impl
201            resources: [$($resource),*],
202            templates: [$($template),*],
203            read_fallback: []
204        );
205    };
206    (
207        resources: [ $($resource:expr),* $(,)? ],
208        templates: [ $($template:expr),* $(,)? ],
209        read_fallback: $fallback:expr
210    ) => {
211        $crate::declare_resources!(@impl
212            resources: [$($resource),*],
213            templates: [$($template),*],
214            read_fallback: [$fallback]
215        );
216    };
217
218    (@impl resources: [$($resource:expr),*], templates: [$($template:expr),*], read_fallback: [$($fallback:tt)*]) => {
219        static RESOURCES: ::std::sync::OnceLock<::std::collections::HashMap<::std::string::String, $crate::resource::Resource>>
220            = ::std::sync::OnceLock::new();
221        static COMPILED_TEMPLATE_MATCHERS: ::std::sync::OnceLock<::std::vec::Vec<$crate::resource::CompiledTemplateMatcher>>
222            = ::std::sync::OnceLock::new();
223        static RESOURCES_LIST_CACHE: ::std::sync::OnceLock<::std::vec::Vec<u8>>
224            = ::std::sync::OnceLock::new();
225        static TEMPLATES_LIST_CACHE: ::std::sync::OnceLock<::std::vec::Vec<u8>>
226            = ::std::sync::OnceLock::new();
227
228        fn get_resources() -> &'static ::std::collections::HashMap<::std::string::String, $crate::resource::Resource> {
229            RESOURCES.get_or_init(|| {
230                let mut map = ::std::collections::HashMap::new();
231                $(
232                    let resource = $resource;
233                    map.insert(resource.uri.clone(), resource);
234                )*
235                map
236            })
237        }
238
239        fn get_template_matchers() -> &'static ::std::vec::Vec<$crate::resource::CompiledTemplateMatcher> {
240            COMPILED_TEMPLATE_MATCHERS.get_or_init(|| {
241                vec![$($template),*]
242                    .into_iter()
243                    .map(|t| $crate::resource::CompiledTemplateMatcher::new(t)
244                        .expect("invalid URI template in declare_resources!"))
245                    .collect()
246            })
247        }
248
249        fn get_resources_list_cache() -> &'static [u8] {
250            RESOURCES_LIST_CACHE.get_or_init(|| {
251                let resources = get_resources();
252                let items: ::std::vec::Vec<$crate::serde_json::Value> = resources
253                    .values()
254                    .map(|r| r.to_list_item())
255                    .collect();
256                $crate::utils::resource_list_response(items, None)
257                    .to_string().into_bytes()
258            })
259        }
260
261        fn get_templates_list_cache() -> &'static [u8] {
262            TEMPLATES_LIST_CACHE.get_or_init(|| {
263                let matchers = get_template_matchers();
264                let items: ::std::vec::Vec<$crate::serde_json::Value> = matchers
265                    .iter()
266                    .map(|m| m.template.to_template_list_item())
267                    .collect();
268                $crate::utils::resource_template_list_response(items, None)
269                    .to_string().into_bytes()
270            })
271        }
272
273        fn read_fallback_handler() -> ::std::option::Option<$crate::resource::GenericResourceReadHandler> {
274            $crate::__declare_plugin_option!($($fallback)*)
275        }
276
277        $crate::declare_resources!(@generated_functions);
278    };
279
280    (@generated_functions) => {
281        /// Auto-generated list_resources — returns pre-serialized JSON (one memcpy).
282        #[no_mangle]
283        pub unsafe extern "C" fn generated_list_resources(
284            result_buf: *mut *mut u8,
285            result_len: *mut usize,
286        ) -> i32 {
287            $crate::utils::return_prebuilt(get_resources_list_cache(), result_buf, result_len)
288        }
289
290        /// Auto-generated list_resource_templates — returns pre-serialized JSON (one memcpy).
291        #[no_mangle]
292        pub unsafe extern "C" fn generated_list_resource_templates(
293            result_buf: *mut *mut u8,
294            result_len: *mut usize,
295        ) -> i32 {
296            $crate::utils::return_prebuilt(get_templates_list_cache(), result_buf, result_len)
297        }
298
299        /// Auto-generated read_resource function
300        ///
301        /// Dispatches: exact static URI, then pre-compiled URI template matchers,
302        /// then optional read_fallback. Template regexes are compiled once (via
303        /// `OnceLock`) on the first call, not per request.
304        #[no_mangle]
305        pub unsafe extern "C" fn generated_read_resource(
306            uri_ptr: *const u8,
307            uri_len: usize,
308            result_buf: *mut *mut u8,
309            result_len: *mut usize,
310        ) -> i32 {
311            let uri_slice = ::std::slice::from_raw_parts(uri_ptr, uri_len);
312            let uri = match ::std::str::from_utf8(uri_slice) {
313                Ok(s) => s,
314                Err(_) => return $crate::utils::return_error(
315                    "Invalid URI encoding",
316                    result_buf,
317                    result_len
318                ),
319            };
320
321            let resources = get_resources();
322            if let Some(resource) = resources.get(uri) {
323                return match (resource.handler)(uri) {
324                    Ok(contents) => {
325                        let response = $crate::utils::resource_read_response(&contents);
326                        $crate::utils::return_success(response, result_buf, result_len)
327                    }
328                    Err(e) => $crate::utils::return_error(&e, result_buf, result_len),
329                };
330            }
331
332            for matcher in get_template_matchers() {
333                if let Some(vars) = matcher.match_uri(uri) {
334                    return match (matcher.template.handler)(uri, &vars) {
335                        Ok(contents) => {
336                            let response = $crate::utils::resource_read_response(&contents);
337                            $crate::utils::return_success(response, result_buf, result_len)
338                        }
339                        Err(e) => $crate::utils::return_error(&e, result_buf, result_len),
340                    };
341                }
342            }
343
344            if let Some(fallback) = read_fallback_handler() {
345                return match fallback(uri) {
346                    Ok(contents) => {
347                        let response = $crate::utils::resource_read_response(&contents);
348                        $crate::utils::return_success(response, result_buf, result_len)
349                    }
350                    Err(e) => $crate::utils::return_error(&e, result_buf, result_len),
351                };
352            }
353
354            $crate::utils::return_error(
355                &format!("Unknown resource: {}", uri),
356                result_buf,
357                result_len
358            )
359        }
360    };
361}