#elicitation #codegen #mcp #macro

macro elicitation_macros

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

23 releases (5 breaking)

Uses new Rust 2024

new 0.9.0 Mar 5, 2026
0.8.3 Mar 2, 2026
0.8.2 Feb 23, 2026
0.7.0 Feb 14, 2026
0.4.8 Feb 5, 2026

#2317 in Procedural macros


Used in 6 crates (via elicitation)

Apache-2.0 OR MIT

27KB
248 lines

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 methodPluginRegistry::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.

Dependencies

~120–495KB
~12K SLoC