pmcp-macros 0.1.0

Procedural macros for PMCP SDK - Model Context Protocol
Documentation

PMCP Macros

Procedural macros for the PMCP (Production MCP) SDK, providing ergonomic tool and handler definitions with automatic schema generation.

Features

  • 🔧 #[tool] - Define individual tools with automatic schema generation
  • 🚀 #[tool_router] - Collect tools from impl blocks for easy registration
  • 📝 Type-safe parameter handling with compile-time validation
  • 🔄 Automatic JSON schema generation from Rust types
  • ⚡ Zero runtime overhead - all code generation happens at compile time

Installation

Add to your Cargo.toml:

[dependencies]
pmcp = { version = "0.6", features = ["macros"] }
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"

Usage

Basic Tool Definition

use pmcp_macros::{tool, tool_router};
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;

#[derive(Debug, Deserialize, JsonSchema)]
struct AddParams {
    a: i32,
    b: i32,
}

#[derive(Debug, Serialize, JsonSchema)]
struct AddResult {
    sum: i32,
}

#[tool(description = "Add two numbers")]
async fn add(params: AddParams) -> Result<AddResult, String> {
    Ok(AddResult {
        sum: params.a + params.b,
    })
}

Tool Router for Multiple Tools

#[derive(Clone)]
struct Calculator;

#[tool_router]
impl Calculator {
    #[tool(description = "Add two numbers")]
    async fn add(&self, a: i32, b: i32) -> Result<i32, String> {
        Ok(a + b)
    }
    
    #[tool(description = "Multiply two numbers")]
    async fn multiply(&self, a: i32, b: i32) -> Result<i32, String> {
        Ok(a * b)
    }
    
    #[tool(name = "div", description = "Divide two numbers")]
    async fn divide(&self, a: f64, b: f64) -> Result<f64, String> {
        if b == 0.0 {
            Err("Division by zero".to_string())
        } else {
            Ok(a / b)
        }
    }
}

Advanced Features

Optional Parameters

#[derive(Debug, Deserialize, JsonSchema)]
struct GreetParams {
    name: String,
    #[serde(default)]
    title: Option<String>,
    #[serde(default)]
    formal: bool,
}

#[tool(description = "Greet a person")]
fn greet(params: GreetParams) -> String {
    match (params.formal, params.title) {
        (true, Some(title)) => format!("Good day, {} {}!", title, params.name),
        (true, None) => format!("Good day, {}!", params.name),
        (false, _) => format!("Hey, {}!", params.name),
    }
}

Complex Types

#[derive(Debug, Deserialize, JsonSchema)]
struct ProcessRequest {
    items: Vec<String>,
    metadata: HashMap<String, Value>,
    config: ProcessConfig,
}

#[derive(Debug, Deserialize, JsonSchema)]
struct ProcessConfig {
    timeout_ms: u64,
    retry_count: u8,
}

#[tool(description = "Process complex data")]
async fn process(req: ProcessRequest) -> Result<Value, Error> {
    // Processing logic here
    Ok(json!({
        "processed": req.items.len(),
        "status": "success"
    }))
}

Attributes

#[tool] Attributes

  • name - Custom tool name (defaults to function name)
  • description - Tool description (required)
  • annotations - Additional metadata (optional)
#[tool(
    name = "custom_name",
    description = "Tool description",
    annotations(
        category = "math",
        complexity = "simple",
        read_only = true
    )
)]

#[tool_router] Attributes

  • router - Name of the router field (defaults to "tool_router")
  • vis - Visibility of generated methods (defaults to pub)
#[tool_router(router = "my_router", vis = "pub(crate)")]
impl MyServer {
    // tools...
}

Schema Generation

The macros automatically generate JSON schemas for your tool parameters using the schemars crate. You can customize the generated schemas using schemars attributes:

#[derive(Deserialize, JsonSchema)]
struct Params {
    #[schemars(description = "User's age in years")]
    #[schemars(range(min = 0, max = 150))]
    age: u8,
    
    #[schemars(regex(pattern = r"^\w+@\w+\.\w+$"))]
    email: String,
    
    #[schemars(length(min = 8, max = 128))]
    password: String,
}

Error Handling

Tools can return Result<T, E> where E implements ToString:

#[tool(description = "Divide two numbers")]
fn divide(a: f64, b: f64) -> Result<f64, MyError> {
    if b == 0.0 {
        Err(MyError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}

#[derive(Debug)]
enum MyError {
    DivisionByZero,
    Overflow,
}

impl ToString for MyError {
    fn to_string(&self) -> String {
        match self {
            Self::DivisionByZero => "Cannot divide by zero".to_string(),
            Self::Overflow => "Operation would overflow".to_string(),
        }
    }
}

Testing

The macro-generated code can be tested like regular Rust code:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]
    async fn test_add_tool() {
        let params = AddParams { a: 5, b: 3 };
        let result = add(params).await.unwrap();
        assert_eq!(result.sum, 8);
    }
}

Performance

  • Zero runtime overhead - all code generation happens at compile time
  • Automatic schema caching for repeated tool calls
  • Efficient parameter parsing with serde
  • No reflection or runtime type information needed

Limitations

  • Currently only supports tools (prompts and resources coming soon)
  • Requires schemars for schema generation
  • Async tools require tokio runtime

Future Plans

  • #[prompt] macro for prompt templates
  • #[resource] macro for resource handlers
  • Custom validation attributes
  • Automatic OpenAPI spec generation
  • Integration with popular web frameworks

License

MIT - See parent project for details