elicitation_macros 0.8.3

Procedural macros for MCP tool generation, tracing instrumentation, and newtype wrapping in the elicitation framework
Documentation

elicitation_macros

Procedural macros that power the elicitation framework's tool-generation, tracing instrumentation, and newtype-wrapping infrastructure.

This crate is re-exported by the main elicitation crate — users import the macros from there, not directly from this crate.

Macros at a glance

Macro Kind Purpose
#[instrumented_impl] attribute on impl Auto-adds #[tracing::instrument] to every method
#[elicit_tools(T1, T2, …)] attribute on impl Generates MCP elicitation tool methods for listed types
#[elicit_trait_tools_router] attribute on impl Wraps trait methods as individually discoverable MCP tools
elicit_safe! declarative Marks a type as safe to surface in MCP schemas
elicit_newtype! declarative Creates an Arc-wrapped newtype over an existing type
reflect_methods! declarative Generates MCP tool boilerplate from method signatures
contract_type attribute on type Attaches requires/ensures verification metadata

#[instrumented_impl]

Adds #[tracing::instrument] to every public method in an impl block, choosing the right instrumentation level automatically:

Method name pattern Instrumentation
new, from_*, try_*, default #[instrument(ret, err)] — logs return value and errors
get, as_*, to_*, into_inner #[instrument(level = "trace", ret)] — low-noise accessor tracing
everything else #[instrument(skip(self))] — spans without logging self
use elicitation::instrumented_impl;

#[instrumented_impl]
impl Config {
    pub fn new(host: String, port: u16) -> Self {}
    pub fn host(&self) -> &str {}
    pub fn validate(&self) -> Result<(), ConfigError> {}
}
// Expands to:
// new          → #[instrument(ret, err)]
// host         → #[instrument(level = "trace", ret)]
// validate     → #[instrument(skip(self))]

Becomes a no-op under #[cfg(kani)] so formal verification harnesses are not disrupted by tracing instrumentation.


#[elicit_tools(T1, T2, …)]

Generates a standalone MCP elicitation endpoint for each listed type. Place it above #[tool_router] on a server impl block:

use elicitation::{elicit_tools, Elicit};
use rmcp::{tool_router, ServerHandler};

#[elicit_tools(ServerConfig, UserCredentials)]
#[tool_router]
impl MyServer {
    // Your other #[tool] methods here
}

For each type T, this generates:

#[tool(description = "Elicit ServerConfig via MCP")]
pub async fn elicit_server_config(
    &self,
    peer: Peer<RoleServer>,
) -> Result<Json<ElicitToolOutput<ServerConfig>>, ErrorData> {
    // drives the sampling conversation then returns the result
}

The ElicitToolOutput<T> wrapper ensures the MCP schema is always an object (never a bare enum or primitive), which rmcp requires.


#[elicit_trait_tools_router(TraitName, field, [method1, method2, …])]

Delegates trait methods to an inner field and registers each as an MCP tool. Useful when you implement a trait on a server struct and want every method individually callable:

use elicitation::elicit_trait_tools_router;

#[elicit_trait_tools_router(HttpClient, client, [get, post, put, delete])]
impl MyServer {
    // client: Box<dyn HttpClient>
}

For each method foo, the macro generates:

  • A FooParams struct (deriving Elicit and JsonSchema)
  • A foo_tool method decorated with #[tool]
  • Delegation: self.client.foo(params.into())

elicit_safe!

Marks a type as safe to surface in MCP schemas — used by the framework to confirm that a type can appear as a tool input or output without violating MCP schema constraints.

use elicitation::elicit_safe;

elicit_safe!(MyNewtype);

Generates a marker trait implementation that the framework checks at compile time before registering a type with the MCP tool registry.


elicit_newtype!

Creates a newtype wrapper that places the inner value behind Arc, making consuming builder types (like reqwest::RequestBuilder) Clone-able across async and MCP tool boundaries:

use elicitation::elicit_newtype;

elicit_newtype!(pub struct RequestBuilder(reqwest::RequestBuilder));
elicit_newtype!(pub struct Response(reqwest::Response));

Generated code:

  • struct RequestBuilder(Arc<reqwest::RequestBuilder>)
  • impl Clone for RequestBuilder — clones the Arc
  • impl Deref / DerefMut — transparent access to the inner type
  • impl From<reqwest::RequestBuilder> for RequestBuilder
  • elicit_safe!(RequestBuilder) — MCP schema safety marker

This is the foundation of the elicit_reqwest crate, where all nine reqwest/http/url types are wrapped this way.


reflect_methods!

Reads the method signatures in an impl block and generates the MCP tool boilerplate for each one — parameter structs with JSON schemas, tool wrapper methods, and type conversions — without any hand-written duplication:

use elicitation::reflect_methods;

elicit_newtype!(pub struct Client(reqwest::Client));

#[reflect_methods]
impl Client {
    pub async fn get(&self, url: Url) -> RequestBuilder {}
    pub async fn post(&self, url: Url) -> RequestBuilder {}
}

For each method foo(param: ParamType) -> ReturnType, the macro generates:

// 1. Parameter struct
#[derive(Elicit, JsonSchema)]
pub struct FooParams { pub param: ParamType }

// 2. Tool wrapper
#[tool(description = "foo")]
pub async fn foo_tool(
    &self,
    params: Parameters<FooParams>,
) -> Result<Json<ReturnType>, ErrorData> {
    self.foo(params.param).await.map(Json).map_err(Into::into)
}

Type conversions applied automatically:

  • &strString
  • &[T]Vec<T>
  • &TT (requires Clone)

Methods returning Self (consuming builders) are skipped — they cannot be wrapped meaningfully as stateless tool calls.


contract_type

An attribute macro that attaches requires and ensures expressions to a type as const-fn methods, making the verification metadata queryable at compile time and readable by the TypeSpec explorer at runtime:

use elicitation::contract_type;

#[contract_type(requires = "value > 0", ensures = "result.get() > 0")]
pub struct I32Positive(i32);

Generates:

impl I32Positive {
    pub const fn __contract_requires() -> &'static str { "value > 0" }
    pub const fn __contract_ensures()  -> &'static str { "result.get() > 0" }
}

These are consumed by the ElicitSpec composition in #[derive(Elicit)] to build the TypeSpec inventory entry for the type automatically.


How the macros compose

A typical newtype MCP plugin is built layer by layer:

elicit_newtype!(Client)          ← 1. Arc-wrap, Clone, Deref
    ↓
#[reflect_methods] impl Client   ← 2. Generate param structs + tool wrappers
    ↓
#[instrumented_impl] impl Client ← 3. Add tracing spans to every method
    ↓
PluginRegistry::new()
    .register("http", ClientPlugin::new())  ← 4. Register with rmcp

For user-defined types the flow is simpler:

#[derive(Elicit, JsonSchema)]    ← 1. Generate Prompt/Survey/Select + elicit_checked()
    ↓
#[elicit_tools(MyType)]          ← 2. (optional) standalone tool endpoint
#[tool_router]
impl MyServer { … }              ← 3. rmcp discovers all #[tool] methods

License

Licensed under either of Apache License 2.0 or MIT License at your option.