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        // Generate a static HashMap of tools using OnceLock for thread-safe lazy init
48        static TOOLS: ::std::sync::OnceLock<::std::collections::HashMap<::std::string::String, $crate::tool::Tool>>
49            = ::std::sync::OnceLock::new();
50
51        fn get_tools() -> &'static ::std::collections::HashMap<::std::string::String, $crate::tool::Tool> {
52            TOOLS.get_or_init(|| {
53                let mut map = ::std::collections::HashMap::new();
54                $(
55                    let tool = $tool;
56                    map.insert(tool.name.clone(), tool);
57                )*
58                map
59            })
60        }
61
62        /// Auto-generated list_tools function
63        ///
64        /// Returns a JSON array of all tool definitions.
65        #[no_mangle]
66        pub unsafe extern "C" fn generated_list_tools(
67            result_buf: *mut *mut u8,
68            result_len: *mut usize,
69        ) -> i32 {
70            let tools = get_tools();
71            let tools_json: ::std::vec::Vec<$crate::serde_json::Value> = tools
72                .values()
73                .filter(|t| t.active)
74                .map(|t| t.to_json_schema())
75                .collect();
76
77            let json_array = $crate::serde_json::Value::Array(tools_json);
78            $crate::utils::return_success(json_array, result_buf, result_len)
79        }
80
81        /// Auto-generated execute_tool function
82        ///
83        /// Dispatches to the appropriate tool handler based on the tool name.
84        #[no_mangle]
85        pub unsafe extern "C" fn generated_execute_tool(
86            tool_name: *const ::std::os::raw::c_char,
87            args_json: *const u8,
88            args_len: usize,
89            result_buf: *mut *mut u8,
90            result_len: *mut usize,
91        ) -> i32 {
92            use ::std::ffi::CStr;
93
94            // Parse tool name
95            let name = match CStr::from_ptr(tool_name).to_str() {
96                Ok(s) => s,
97                Err(_) => return $crate::utils::return_error(
98                    "Invalid tool name encoding",
99                    result_buf,
100                    result_len
101                ),
102            };
103
104            // Parse arguments
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            // Find and execute the tool (O(1) HashMap lookup!)
116            let tools = get_tools();
117            match tools.get(name) {
118                Some(tool) => {
119                    if tool.active {
120                        match (tool.handler)(&args) {
121                            Ok(result) => $crate::utils::return_success(
122                                result,
123                                result_buf,
124                                result_len
125                            ),
126                            Err(e) => $crate::utils::return_error(
127                                &e,
128                                result_buf,
129                                result_len
130                            ),
131                        }
132                    } else {
133                        $crate::utils::return_error(
134                            &format!("Inactive tool: {}", name),
135                            result_buf,
136                            result_len)
137                    }
138                }
139                None => $crate::utils::return_error(
140                    &format!("Unknown tool: {}", name),
141                    result_buf,
142                    result_len
143                ),
144            }
145        }
146    };
147}
148
149/// Declare resources and auto-generate list_resources and read_resource functions
150///
151/// This macro takes a list of Resource definitions and generates:
152/// - A static resource registry (HashMap by URI)
153/// - The `generated_list_resources` function
154/// - The `generated_read_resource` function
155///
156/// These generated functions can be used in the `declare_plugin!` macro with
157/// `list_resources` and `read_resource`.
158///
159/// # Example
160///
161/// ```ignore
162/// use mcp_plugin_api::*;
163///
164/// fn read_readme(uri: &str) -> Result<ResourceContents, String> {
165///     Ok(vec![ResourceContent::text(
166///         uri,
167///         "# Hello",
168///         Some("text/markdown".to_string()),
169///     )])
170/// }
171///
172/// declare_resources! {
173///     resources: [
174///         Resource::builder("file:///docs/readme", read_readme)
175///             .name("readme.md")
176///             .description("Project documentation")
177///             .mime_type("text/markdown")
178///             .build(),
179///     ]
180/// }
181///
182/// declare_plugin! {
183///     list_tools: generated_list_tools,
184///     execute_tool: generated_execute_tool,
185///     free_string: mcp_plugin_api::utils::standard_free_string,
186///     list_resources: generated_list_resources,
187///     read_resource: generated_read_resource
188/// }
189/// ```
190#[macro_export]
191macro_rules! declare_resources {
192    (resources: [ $($resource:expr),* $(,)? ]) => {
193        static RESOURCES: ::std::sync::OnceLock<::std::collections::HashMap<::std::string::String, $crate::resource::Resource>>
194            = ::std::sync::OnceLock::new();
195
196        fn get_resources() -> &'static ::std::collections::HashMap<::std::string::String, $crate::resource::Resource> {
197            RESOURCES.get_or_init(|| {
198                let mut map = ::std::collections::HashMap::new();
199                $(
200                    let resource = $resource;
201                    map.insert(resource.uri.clone(), resource);
202                )*
203                map
204            })
205        }
206
207        /// Auto-generated list_resources function
208        ///
209        /// Returns JSON: `{ "resources": [...], "nextCursor": "..." }`
210        #[no_mangle]
211        pub unsafe extern "C" fn generated_list_resources(
212            result_buf: *mut *mut u8,
213            result_len: *mut usize,
214        ) -> i32 {
215            let resources = get_resources();
216            let resources_json: ::std::vec::Vec<$crate::serde_json::Value> = resources
217                .values()
218                .map(|r| r.to_list_item())
219                .collect();
220
221            let json_array = $crate::serde_json::Value::Array(resources_json);
222            $crate::utils::return_success(json_array, result_buf, result_len)
223        }
224
225        /// Auto-generated read_resource function
226        ///
227        /// Dispatches to the appropriate resource handler based on the URI.
228        #[no_mangle]
229        pub unsafe extern "C" fn generated_read_resource(
230            uri_ptr: *const u8,
231            uri_len: usize,
232            result_buf: *mut *mut u8,
233            result_len: *mut usize,
234        ) -> i32 {
235            let uri_slice = ::std::slice::from_raw_parts(uri_ptr, uri_len);
236            let uri = match ::std::str::from_utf8(uri_slice) {
237                Ok(s) => s.to_string(),
238                Err(_) => return $crate::utils::return_error(
239                    "Invalid URI encoding",
240                    result_buf,
241                    result_len
242                ),
243            };
244
245            let resources = get_resources();
246            match resources.get(&uri) {
247                Some(resource) => {
248                    match (resource.handler)(&uri) {
249                        Ok(contents) => {
250                            let contents_json: ::std::vec::Vec<$crate::serde_json::Value> =
251                                contents.iter().map(|c| c.to_json()).collect();
252                            let result = $crate::serde_json::json!({
253                                "contents": contents_json
254                            });
255                            $crate::utils::return_success(result, result_buf, result_len)
256                        }
257                        Err(e) => $crate::utils::return_error(&e, result_buf, result_len),
258                    }
259                }
260                None => $crate::utils::return_error(
261                    &format!("Unknown resource: {}", uri),
262                    result_buf,
263                    result_len
264                ),
265            }
266        }
267    };
268}