pforge-macro

Procedural macros for the pforge framework - ergonomic attribute macros for building MCP handlers.
Features
#[handler] - Derive Handler trait automatically
#[tool] - Generate complete MCP tool with JSON Schema
- Type-Safe: Compile-time validation of handler signatures
- Zero Boilerplate: Write handlers as plain async functions
- Auto Schema: Automatic JSON Schema generation from Rust types
Installation
cargo add pforge-macro
Usage
Basic Handler
use pforge_macro::handler;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Deserialize)]
struct GreetParams {
name: String,
}
#[handler]
async fn greet(params: GreetParams) -> Result<Value> {
Ok(json!({
"message": format!("Hello, {}!", params.name)
}))
}
Expands to:
struct GreetHandler;
#[async_trait::async_trait]
impl Handler for GreetHandler {
async fn handle(&self, params: Value) -> Result<Value> {
let params: GreetParams = serde_json::from_value(params)?;
Ok(json!({
"message": format!("Hello, {}", params.name)
}))
}
}
Tool Macro
The #[tool] macro generates everything needed for an MCP tool:
use pforge_macro::tool;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[tool(
name = "calculator",
description = "Perform basic arithmetic"
)]
async fn calculate(
#[doc = "First operand"] a: f64,
#[doc = "Second operand"] b: f64,
#[doc = "Operation: add, sub, mul, div"] op: String,
) -> Result<f64> {
match op.as_str() {
"add" => Ok(a + b),
"sub" => Ok(a - b),
"mul" => Ok(a * b),
"div" if b != 0.0 => Ok(a / b),
_ => Err(Error::Validation("Invalid operation".into())),
}
}
This generates:
- Parameter Struct: Type-safe params with JSON Schema
- Handler Implementation: Full Handler trait
- Schema Function: JSON Schema for MCP protocol
- Registration Helper: Easy registry integration
Derive Macros
use pforge_macro::Handler;
use serde::{Deserialize, Serialize};
#[derive(Handler)]
struct MyHandler {
config: Config,
}
#[async_trait::async_trait]
impl MyHandler {
async fn handle(&self, params: Value) -> Result<Value> {
Ok(json!({"status": "ok"}))
}
}
Attributes
#[handler]
Mark async functions as MCP handlers:
#[handler]
async fn my_handler(params: MyParams) -> Result<Value> {
}
Requirements:
- Must be
async fn
- First parameter must be deserializable from JSON
- Must return
Result<Value> or Result<impl Serialize>
#[tool]
Full MCP tool generation:
#[tool(
name = "tool_name", // Required
description = "Description", // Required
timeout_ms = 5000, // Optional
)]
async fn my_tool(param1: Type1, param2: Type2) -> Result<ReturnType> {
}
Generates:
- Parameter struct with
JsonSchema
- Handler implementation
- Schema function
- Registration code
#[derive(Handler)]
Derive Handler trait for custom types:
#[derive(Handler)]
#[handler(
validate = true, // Add validation middleware
log = true, // Add logging middleware
timeout_ms = 10000, // Set timeout
)]
struct CustomHandler {
}
Advanced Examples
Stateful Handler
use pforge_macro::tool;
use std::sync::Arc;
use tokio::sync::RwLock;
struct Counter {
value: Arc<RwLock<i64>>,
}
#[tool(
name = "increment",
description = "Increment counter"
)]
impl Counter {
async fn increment(&self, amount: i64) -> Result<i64> {
let mut value = self.value.write().await;
*value += amount;
Ok(*value)
}
}
Validation
use pforge_macro::tool;
use validator::Validate;
#[derive(Deserialize, Validate)]
struct UserInput {
#[validate(email)]
email: String,
#[validate(range(min = 18, max = 120))]
age: u8,
}
#[tool(
name = "create_user",
description = "Create a new user"
)]
async fn create_user(input: UserInput) -> Result<Value> {
input.validate()?;
Ok(json!({"id": "user_123"}))
}
Error Handling
use pforge_macro::tool;
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
#[error("User not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
Invalid(String),
}
#[tool(
name = "get_user",
description = "Fetch user by ID"
)]
async fn get_user(id: String) -> Result<User, MyError> {
database::find_user(&id)
.await
.ok_or_else(|| MyError::NotFound(id))
}
Generated Code Quality
All macro-generated code:
- ✅ Follows Rust naming conventions
- ✅ Includes proper error handling
- ✅ Generates complete documentation
- ✅ Passes clippy lints
- ✅ Is fully type-safe
Compile-Time Checks
The macros perform validation at compile time:
- Signature Validation: Ensures async fn with correct return type
- Type Bounds: Verifies Serialize/Deserialize bounds
- Attribute Validation: Checks required attributes are present
- Name Conflicts: Prevents duplicate tool names
Documentation
License
MIT