pmcp-macros
Procedural macros that eliminate boilerplate for MCP tools, prompts, resources, and
server routers. Shipped as part of pmcp via the macros feature flag. All four
macros plug into pmcp's compile-time schema generation, State<T> injection, and
handler registry, so your code focuses on behavior instead of protocol plumbing.
Installation
Add pmcp with the macros feature. This pulls pmcp-macros transitively — you
do not need to add it as a direct dependency.
[]
= { = "2.3", = ["macros"] }
= "1.0"
= { = "1.0", = ["derive"] }
= { = "1.46", = ["full"] }
Overview
pmcp-macros provides four attribute macros:
| Macro | Applied to | Purpose |
|---|---|---|
#[mcp_tool] |
async fn or fn |
Define a tool handler with a typed arg struct and compile-time schema |
#[mcp_server] |
impl block |
Collect tools and prompts on a type into a single registerable McpServer |
#[mcp_prompt] |
async fn or fn |
Define a prompt template with typed arguments and auto-generated argument schema |
#[mcp_resource] |
async fn or fn |
Define a resource handler with URI template matching and parameter extraction |
Each macro generates the glue code and schema wiring that MCP servers need. Schemas
come from schemars::JsonSchema derives on your argument and result types, so the
wire protocol stays in sync with your Rust types automatically.
#[mcp_tool]
Purpose
#[mcp_tool] turns a plain async or sync function into a full ToolHandler with a
compile-time JSON Schema derived from its argument struct. The macro eliminates the
Box::pin(async move { ... }) boilerplate required by manual ToolHandler impls,
enforces a description at compile time, and supports shared state via State<T>.
Attributes
description = "..."— required. Human-readable description exposed via the MCPtools/listresponse.name = "..."— optional. Defaults to the function name.annotations(...)— optional. MCP standard annotations:read_only = bool,destructive = bool,idempotent = bool,open_world = bool.ui = "..."— optional. Widget resource URI for MCP Apps integrations.
Example
use ;
use JsonSchema;
use ;
async
async
The macro expands the annotated function into a zero-arg constructor (add()) that
returns a generated AddTool struct implementing ToolHandler. Register it with
ServerBuilder::tool(name, handler).
Shared state with State<T>
Tools that need access to shared data (a database handle, a config struct, a client
pool) can take a State<T> parameter. The macro detects it automatically and the
builder wires it in at registration time via .with_state(...). No Arc::clone,
no move captures — just declare the dependency in the function signature.
use ;
use JsonSchema;
use ;
use ;
use Arc;
async
async
Sync tools work the same way — declare a plain fn instead of async fn. The macro
auto-detects and wraps it accordingly.
Full runnable example
See the complete showcase covering standalone tools, state injection, annotations, and impl-block tools: https://github.com/paiml/pmcp/blob/main/examples/s23_mcp_tool_macro.rs
#[mcp_server]
Purpose
#[mcp_server] promotes an impl block into a server bundle. Every method
annotated with #[mcp_tool] or #[mcp_prompt] inside the block is collected,
ToolHandler/PromptHandler structs are generated for each, and an
impl McpServer for YourType is added so ServerBuilder::mcp_server(...) can
register them all at once. A single Arc<YourType> is shared across every handler,
so &self state is accessible for free.
Example
use ;
use JsonSchema;
use ;
use ;
;
async
Mix #[mcp_tool] and #[mcp_prompt] freely within the same impl block — the macro
routes each method to the appropriate handler type. Use standalone .tool(...) /
.prompt(...) calls alongside .mcp_server(...) for one-off handlers that do not
belong to the type.
Full runnable example
examples/s23_mcp_tool_macro.rs also demonstrates #[mcp_server] alongside
standalone #[mcp_tool] registrations:
https://github.com/paiml/pmcp/blob/main/examples/s23_mcp_tool_macro.rs
#[mcp_prompt]
Purpose
#[mcp_prompt] defines a prompt template without the
HashMap::get("x").ok_or()?.parse()? boilerplate of hand-rolled PromptHandler
implementations. Arguments are derived from a struct that implements JsonSchema,
so the argument descriptions surface in the MCP prompts/list response without
duplication. MCP sends prompt arguments as strings, and the SDK coerces them into
numeric, boolean, or optional types automatically based on the struct definition.
Attributes
description = "..."— required. Human-readable description.name = "..."— optional. Defaults to the function name.
Example
use ;
use ;
use JsonSchema;
use Deserialize;
async
async
Like tools, prompts support State<T> injection — just add a State<YourType>
parameter to the function signature and wire it at registration with
code_review().with_state(shared). Optional prompt arguments use Option<T> on the
struct field; the SDK fills in absent values with None automatically.
Full runnable example
https://github.com/paiml/pmcp/blob/main/examples/s24_mcp_prompt_macro.rs
#[mcp_resource]
Note:
#[mcp_resource]is currently imported directly frompmcp_macrosrather than re-exported viapmcp. A futurepmcprelease will add it to the re-export alongside the other three macros, at which point the import can becomeuse pmcp::mcp_resource;. Everything else about the macro is the same.
Purpose
#[mcp_resource] defines a resource handler whose URI can carry template variables.
The macro generates a struct implementing DynamicResourceProvider, so at request
time an incoming URI is pattern-matched against the template and any captured
segments are handed to the function as typed parameters. No hand-rolled URI parsing,
no string indexing.
Attributes
uri = "..."— required. URI or URI template. Templates use RFC 6570-style{variable_name}placeholders (for exampledocs://{topic}).description = "..."— required. Human-readable description.name = "..."— optional. Defaults to the function name.mime_type = "..."— optional. Defaults to"text/plain".
URI template variables
The uri attribute accepts a URI template such as docs://{topic} or
data://articles/{topic}. Every {variable_name} placeholder inside the template
is automatically extracted at request time and passed to the decorated function
as a parameter of type String. The function parameter name must match the
template variable name exactly — the macro uses the name to route captured
substrings into the correct argument slot. You never have to parse the URI yourself.
For example, a resource declared as:
#[mcp_resource(uri = "docs://{topic}", description = "Documentation pages")]
async fn read_doc(topic: String) -> pmcp::Result<String> { ... }
will receive the topic segment of a request URI like docs://rust-macros bound
directly to the topic: String parameter. Multiple placeholders are supported —
declare one String parameter per {variable_name} in the template.
Example
// Note: direct import until the `mcp_resource` re-export gap is closed.
use mcp_resource;
async
Resources also support State<T> injection the same way tools and prompts do —
declare db: State<MyDatabase> alongside the template-variable parameters and the
SDK wires the shared state in at registration time.
Feature flags
The macros feature flag on pmcp exists so users who do not need proc-macro
machinery can ship with a smaller compile surface. If you want any of the four
macros, enable macros. There is no reason to add pmcp-macros as a direct
dependency — always route through pmcp's feature flag.