mcpkit_macros/
lib.rs

1//! Procedural macros for the MCP SDK.
2//!
3//! This crate provides the unified `#[mcp_server]` macro that simplifies
4//! MCP server development.
5//!
6//! # Overview
7//!
8//! The macro system provides:
9//!
10//! - `#[mcp_server]` - Transform an impl block into a full MCP server
11//! - `#[tool]` - Mark a method as an MCP tool
12//! - `#[resource]` - Mark a method as an MCP resource handler
13//! - `#[prompt]` - Mark a method as an MCP prompt handler
14//!
15//! # Example
16//!
17//! ```ignore
18//! use mcpkit::prelude::*;
19//! use mcpkit::transport::stdio::StdioTransport;
20//!
21//! struct Calculator;
22//!
23//! #[mcp_server(name = "calculator", version = "1.0.0")]
24//! impl Calculator {
25//!     /// Add two numbers together
26//!     #[tool(description = "Add two numbers")]
27//!     async fn add(&self, a: f64, b: f64) -> ToolOutput {
28//!         ToolOutput::text((a + b).to_string())
29//!     }
30//!
31//!     /// Multiply two numbers
32//!     #[tool(description = "Multiply two numbers")]
33//!     async fn multiply(&self, a: f64, b: f64) -> ToolOutput {
34//!         ToolOutput::text((a * b).to_string())
35//!     }
36//! }
37//!
38//! #[tokio::main]
39//! async fn main() -> Result<(), McpError> {
40//!     let transport = StdioTransport::new();
41//!     let server = ServerBuilder::new(Calculator)
42//!         .with_tools(Calculator)
43//!         .build();
44//!     server.serve(transport).await
45//! }
46//! ```
47//!
48//! # Code Reduction
49//!
50//! This single macro replaces 4 separate macros:
51//! - `#[derive(Clone)]` with manual router field
52//! - `#[tool_router]`
53//! - `#[tool_handler]`
54//! - Manual `new()` constructor
55//!
56//! **Result: Reduced boilerplate code.**
57
58#![deny(missing_docs)]
59
60mod attrs;
61mod client;
62mod codegen;
63mod derive;
64mod error;
65mod prompt;
66mod resource;
67mod server;
68mod tool;
69
70use proc_macro::TokenStream;
71
72/// The unified MCP server macro.
73///
74/// This macro transforms an impl block into a full MCP server implementation,
75/// automatically generating all the necessary trait implementations and routing.
76///
77/// # Attributes
78///
79/// - `name` - Server name (required)
80/// - `version` - Server version (required, can use `env!("CARGO_PKG_VERSION")`)
81/// - `instructions` - Optional usage instructions sent to clients
82/// - `capabilities` - Optional list of capabilities to advertise
83/// - `debug_expand` - Set to `true` to print generated code (default: false)
84///
85/// # Example
86///
87/// ```ignore
88/// #[mcp_server(name = "my-server", version = "1.0.0")]
89/// impl MyServer {
90///     #[tool(description = "Do something")]
91///     async fn my_tool(&self, input: String) -> ToolOutput {
92///         ToolOutput::text(format!("Got: {}", input))
93///     }
94/// }
95/// ```
96///
97/// # Generated Code
98///
99/// The macro generates:
100///
101/// 1. `impl ServerHandler` with `server_info()` and `capabilities()`
102/// 2. `impl ToolHandler` with `list_tools()` and `call_tool()` (if any `#[tool]` methods)
103/// 3. `impl ResourceHandler` (if any `#[resource]` methods)
104/// 4. `impl PromptHandler` (if any `#[prompt]` methods)
105///
106/// To serve the MCP server, use `ServerBuilder` with your preferred transport:
107///
108/// ```ignore
109/// let server = ServerBuilder::new(MyServer).with_tools(MyServer).build();
110/// server.serve(StdioTransport::new()).await?;
111/// ```
112#[proc_macro_attribute]
113pub fn mcp_server(attr: TokenStream, item: TokenStream) -> TokenStream {
114    server::expand_mcp_server(attr.into(), item.into())
115        .unwrap_or_else(|e| e.to_compile_error())
116        .into()
117}
118
119/// Mark a method as an MCP tool.
120///
121/// This attribute is used inside an `#[mcp_server]` impl block to designate
122/// a method as an MCP tool that AI assistants can call.
123///
124/// # Attributes
125///
126/// - `description` - Required description of what the tool does
127/// - `name` - Override the tool name (defaults to the method name)
128///
129/// ## Tool Annotations (Hints for AI Assistants)
130///
131/// These attributes provide hints to AI assistants about the tool's behavior.
132/// They appear in the tool's JSON schema as `annotations`:
133///
134/// - `destructive = true` - The tool may cause irreversible changes (e.g., delete files,
135///   drop tables, send emails). AI assistants may ask for confirmation before calling.
136///
137/// - `idempotent = true` - Calling the tool multiple times with the same arguments
138///   produces the same result (safe to retry on failure).
139///
140/// - `read_only = true` - The tool only reads data and has no side effects.
141///   AI assistants may call these tools more freely.
142///
143/// ```ignore
144/// // A destructive tool - deletes data
145/// #[tool(description = "Delete a user account", destructive = true)]
146/// async fn delete_user(&self, user_id: String) -> ToolOutput { ... }
147///
148/// // A read-only tool - safe to call repeatedly
149/// #[tool(description = "Get user profile", read_only = true)]
150/// async fn get_user(&self, user_id: String) -> ToolOutput { ... }
151///
152/// // An idempotent tool - safe to retry
153/// #[tool(description = "Set user email", idempotent = true)]
154/// async fn set_email(&self, user_id: String, email: String) -> ToolOutput { ... }
155/// ```
156///
157/// # Parameter Extraction
158///
159/// Tool parameters are extracted directly from the function signature:
160///
161/// ```ignore
162/// #[tool(description = "Search for items")]
163/// async fn search(
164///     &self,
165///     /// The search query  (becomes JSON Schema description)
166///     query: String,
167///     /// Maximum results to return
168///     #[mcp(default = 10)]
169///     limit: usize,
170///     /// Optional category filter
171///     category: Option<String>,
172/// ) -> ToolOutput {
173///     // ...
174/// }
175/// ```
176///
177/// # Return Types
178///
179/// Tools can return either `ToolOutput` or `Result<ToolOutput, McpError>`:
180///
181/// ## Using `ToolOutput` directly
182///
183/// Use this when you want to handle errors as recoverable user-facing messages:
184///
185/// ```ignore
186/// #[tool(description = "Divide two numbers")]
187/// async fn divide(&self, a: f64, b: f64) -> ToolOutput {
188///     if b == 0.0 {
189///         // User sees this as a tool error they can recover from
190///         return ToolOutput::error("Cannot divide by zero");
191///     }
192///     ToolOutput::text(format!("{}", a / b))
193/// }
194/// ```
195///
196/// ## Using `Result<ToolOutput, McpError>`
197///
198/// Use this for errors that should propagate as JSON-RPC errors (e.g., invalid
199/// parameters, resource not found, permission denied):
200///
201/// ```ignore
202/// #[tool(description = "Read a file")]
203/// async fn read_file(&self, path: String) -> Result<ToolOutput, McpError> {
204///     // Parameter validation - returns JSON-RPC error
205///     if path.contains("..") {
206///         return Err(McpError::invalid_params("read_file", "Path traversal not allowed"));
207///     }
208///
209///     // Resource access - returns JSON-RPC error
210///     let content = std::fs::read_to_string(&path)
211///         .map_err(|e| McpError::resource_not_found(&path))?;
212///
213///     Ok(ToolOutput::text(content))
214/// }
215/// ```
216///
217/// ## When to use which
218///
219/// | Scenario | Return Type | Example |
220/// |----------|-------------|---------|
221/// | User input can be corrected | `ToolOutput::error()` | "Please provide a valid email" |
222/// | Invalid parameters | `Err(McpError::invalid_params())` | Missing required field |
223/// | Resource not found | `Err(McpError::resource_not_found())` | File doesn't exist |
224/// | Permission denied | `Err(McpError::resource_access_denied())` | No read access |
225/// | Internal server error | `Err(McpError::internal())` | Database connection failed |
226#[proc_macro_attribute]
227pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
228    tool::expand_tool(attr.into(), item.into())
229        .unwrap_or_else(|e| e.to_compile_error())
230        .into()
231}
232
233/// Mark a method as an MCP resource handler.
234///
235/// This attribute designates a method that provides access to resources
236/// that AI assistants can read.
237///
238/// # Attributes
239///
240/// - `uri_pattern` - The URI pattern for this resource (e.g., `"myserver://data/{id}"`)
241/// - `name` - Human-readable name for the resource
242/// - `description` - Description of the resource
243/// - `mime_type` - MIME type of the resource content
244///
245/// # Example
246///
247/// ```ignore
248/// #[resource(
249///     uri_pattern = "config://app/{key}",
250///     name = "App Configuration",
251///     description = "Application configuration values",
252///     mime_type = "application/json"
253/// )]
254/// async fn get_config(&self, key: String) -> ResourceContents {
255///     // ...
256/// }
257/// ```
258#[proc_macro_attribute]
259pub fn resource(attr: TokenStream, item: TokenStream) -> TokenStream {
260    resource::expand_resource(attr.into(), item.into())
261        .unwrap_or_else(|e| e.to_compile_error())
262        .into()
263}
264
265/// Mark a method as an MCP prompt handler.
266///
267/// This attribute designates a method that provides prompt templates
268/// that AI assistants can use.
269///
270/// # Attributes
271///
272/// - `description` - Description of what the prompt does
273/// - `name` - Override the prompt name (defaults to the method name)
274///
275/// # Example
276///
277/// ```ignore
278/// #[prompt(description = "Generate a greeting message")]
279/// async fn greeting(&self, name: String) -> GetPromptResult {
280///     GetPromptResult {
281///         description: Some("A friendly greeting".to_string()),
282///         messages: vec![
283///             PromptMessage::user(format!("Hello, {}!", name))
284///         ],
285///     }
286/// }
287/// ```
288#[proc_macro_attribute]
289pub fn prompt(attr: TokenStream, item: TokenStream) -> TokenStream {
290    prompt::expand_prompt(attr.into(), item.into())
291        .unwrap_or_else(|e| e.to_compile_error())
292        .into()
293}
294
295/// Derive macro for tool input types.
296///
297/// This derive macro generates JSON Schema information for complex
298/// tool input types.
299///
300/// # Example
301///
302/// ```ignore
303/// #[derive(ToolInput)]
304/// struct SearchInput {
305///     /// The search query
306///     query: String,
307///     /// Maximum results (1-100)
308///     #[mcp(default = 10, range(1, 100))]
309///     limit: usize,
310///     /// Optional filters
311///     filters: Option<Vec<String>>,
312/// }
313/// ```
314#[proc_macro_derive(ToolInput, attributes(mcp))]
315pub fn derive_tool_input(input: TokenStream) -> TokenStream {
316    derive::expand_tool_input(input.into())
317        .unwrap_or_else(|e| e.to_compile_error())
318        .into()
319}
320
321// =============================================================================
322// Client Macros
323// =============================================================================
324
325/// The unified MCP client macro.
326///
327/// This macro transforms an impl block into a `ClientHandler` implementation,
328/// automatically generating all the necessary trait implementations.
329///
330/// # Example
331///
332/// ```ignore
333/// use mcpkit::prelude::*;
334///
335/// struct MyClient;
336///
337/// #[mcp_client]
338/// impl MyClient {
339///     /// Handle LLM sampling requests from servers.
340///     #[sampling]
341///     async fn handle_sampling(
342///         &self,
343///         request: CreateMessageRequest,
344///     ) -> Result<CreateMessageResult, McpError> {
345///         // Call your LLM here
346///         Ok(CreateMessageResult {
347///             role: Role::Assistant,
348///             content: Content::text("Response"),
349///             model: "my-model".to_string(),
350///             stop_reason: Some("end_turn".to_string()),
351///         })
352///     }
353///
354///     /// Provide filesystem roots to servers.
355///     #[roots]
356///     fn get_roots(&self) -> Vec<Root> {
357///         vec![Root::new("file:///home/user/project").name("Project")]
358///     }
359/// }
360/// ```
361///
362/// # Handler Methods
363///
364/// The following attributes mark methods as handlers:
365///
366/// - `#[sampling]` - Handle `sampling/createMessage` requests
367/// - `#[elicitation]` - Handle `elicitation/elicit` requests
368/// - `#[roots]` - Handle `roots/list` requests
369/// - `#[on_connected]` - Called when connection is established
370/// - `#[on_disconnected]` - Called when connection is closed
371/// - `#[on_task_progress]` - Handle task progress notifications
372/// - `#[on_resource_updated]` - Handle resource update notifications
373/// - `#[on_tools_list_changed]` - Handle tools list change notifications
374/// - `#[on_resources_list_changed]` - Handle resources list change notifications
375/// - `#[on_prompts_list_changed]` - Handle prompts list change notifications
376///
377/// # Generated Code
378///
379/// The macro generates:
380///
381/// 1. `impl ClientHandler` with all handler methods delegating to your implementations
382/// 2. A `capabilities()` method returning the appropriate `ClientCapabilities`
383#[proc_macro_attribute]
384pub fn mcp_client(attr: TokenStream, item: TokenStream) -> TokenStream {
385    client::expand_mcp_client(attr.into(), item.into())
386        .unwrap_or_else(|e| e.to_compile_error())
387        .into()
388}
389
390/// Mark a method as a sampling handler.
391///
392/// This handler is called when servers request LLM completions.
393/// The method should accept a `CreateMessageRequest` and return
394/// `Result<CreateMessageResult, McpError>` or `CreateMessageResult`.
395///
396/// # Example
397///
398/// ```ignore
399/// #[sampling]
400/// async fn handle_sampling(
401///     &self,
402///     request: CreateMessageRequest,
403/// ) -> Result<CreateMessageResult, McpError> {
404///     // Process the request and generate a response
405/// }
406/// ```
407#[proc_macro_attribute]
408pub fn sampling(_attr: TokenStream, item: TokenStream) -> TokenStream {
409    // This is a marker attribute - just pass through the item unchanged
410    item
411}
412
413/// Mark a method as an elicitation handler.
414///
415/// This handler is called when servers request user input.
416/// The method should accept an `ElicitRequest` and return
417/// `Result<ElicitResult, McpError>` or `ElicitResult`.
418///
419/// # Example
420///
421/// ```ignore
422/// #[elicitation]
423/// async fn handle_elicitation(
424///     &self,
425///     request: ElicitRequest,
426/// ) -> Result<ElicitResult, McpError> {
427///     // Present the request to the user and return their response
428/// }
429/// ```
430#[proc_macro_attribute]
431pub fn elicitation(_attr: TokenStream, item: TokenStream) -> TokenStream {
432    // This is a marker attribute - just pass through the item unchanged
433    item
434}
435
436/// Mark a method as a roots handler.
437///
438/// This handler is called when servers request the list of filesystem roots.
439/// The method should return `Vec<Root>` or `Result<Vec<Root>, McpError>`.
440///
441/// # Example
442///
443/// ```ignore
444/// #[roots]
445/// fn get_roots(&self) -> Vec<Root> {
446///     vec![
447///         Root::new("file:///home/user/project").name("Project"),
448///         Root::new("file:///home/user/docs").name("Documents"),
449///     ]
450/// }
451/// ```
452#[proc_macro_attribute]
453pub fn roots(_attr: TokenStream, item: TokenStream) -> TokenStream {
454    // This is a marker attribute - just pass through the item unchanged
455    item
456}
457
458/// Mark a method as the connection established handler.
459///
460/// This handler is called when the client connects to a server.
461///
462/// # Example
463///
464/// ```ignore
465/// #[on_connected]
466/// async fn handle_connected(&self) {
467///     println!("Connected to server!");
468/// }
469/// ```
470#[proc_macro_attribute]
471pub fn on_connected(_attr: TokenStream, item: TokenStream) -> TokenStream {
472    // This is a marker attribute - just pass through the item unchanged
473    item
474}
475
476/// Mark a method as the disconnection handler.
477///
478/// This handler is called when the client disconnects from a server.
479///
480/// # Example
481///
482/// ```ignore
483/// #[on_disconnected]
484/// async fn handle_disconnected(&self) {
485///     println!("Disconnected from server");
486/// }
487/// ```
488#[proc_macro_attribute]
489pub fn on_disconnected(_attr: TokenStream, item: TokenStream) -> TokenStream {
490    // This is a marker attribute - just pass through the item unchanged
491    item
492}
493
494/// Mark a method as a task progress notification handler.
495///
496/// This handler is called when the server reports progress on a task.
497/// The method should accept a `TaskId` and `TaskProgress`.
498///
499/// # Example
500///
501/// ```ignore
502/// #[on_task_progress]
503/// async fn handle_progress(&self, task_id: TaskId, progress: TaskProgress) {
504///     println!("Task {} is {}% complete", task_id, progress.progress * 100.0);
505/// }
506/// ```
507#[proc_macro_attribute]
508pub fn on_task_progress(_attr: TokenStream, item: TokenStream) -> TokenStream {
509    // This is a marker attribute - just pass through the item unchanged
510    item
511}
512
513/// Mark a method as a resource update notification handler.
514///
515/// This handler is called when a subscribed resource is updated.
516/// The method should accept a `String` (the resource URI).
517///
518/// # Example
519///
520/// ```ignore
521/// #[on_resource_updated]
522/// async fn handle_resource_updated(&self, uri: String) {
523///     println!("Resource updated: {}", uri);
524///     // Invalidate cache, refresh data, etc.
525/// }
526/// ```
527#[proc_macro_attribute]
528pub fn on_resource_updated(_attr: TokenStream, item: TokenStream) -> TokenStream {
529    // This is a marker attribute - just pass through the item unchanged
530    item
531}
532
533/// Mark a method as a tools list change notification handler.
534///
535/// This handler is called when the server's tool list changes.
536///
537/// # Example
538///
539/// ```ignore
540/// #[on_tools_list_changed]
541/// async fn handle_tools_changed(&self) {
542///     println!("Tools list changed - refreshing cache");
543/// }
544/// ```
545#[proc_macro_attribute]
546pub fn on_tools_list_changed(_attr: TokenStream, item: TokenStream) -> TokenStream {
547    // This is a marker attribute - just pass through the item unchanged
548    item
549}
550
551/// Mark a method as a resources list change notification handler.
552///
553/// This handler is called when the server's resource list changes.
554///
555/// # Example
556///
557/// ```ignore
558/// #[on_resources_list_changed]
559/// async fn handle_resources_changed(&self) {
560///     println!("Resources list changed - refreshing cache");
561/// }
562/// ```
563#[proc_macro_attribute]
564pub fn on_resources_list_changed(_attr: TokenStream, item: TokenStream) -> TokenStream {
565    // This is a marker attribute - just pass through the item unchanged
566    item
567}
568
569/// Mark a method as a prompts list change notification handler.
570///
571/// This handler is called when the server's prompt list changes.
572///
573/// # Example
574///
575/// ```ignore
576/// #[on_prompts_list_changed]
577/// async fn handle_prompts_changed(&self) {
578///     println!("Prompts list changed - refreshing cache");
579/// }
580/// ```
581#[proc_macro_attribute]
582pub fn on_prompts_list_changed(_attr: TokenStream, item: TokenStream) -> TokenStream {
583    // This is a marker attribute - just pass through the item unchanged
584    item
585}