Skip to content

#[prompt] macro doesn't respect local feature — unconditionally emits + Send unlike #[tool] #801

@WeekendSuperhero

Description

@WeekendSuperhero

Summary

The #[tool] macro correctly omits + Send on generated handler futures when the local crate feature is enabled or #[tool(local)] is used. The #[prompt] macro does not — it unconditionally emits + Send on all generated futures, making it impossible to use !Send types in prompt handlers even with the local feature.

Context

The local feature was added in #740 to support !Send tool handlers (e.g., single-threaded platform APIs like Apple's EventKit on macOS). The #[tool] macro was updated to check for the feature:

rmcp-macros/src/tool.rs:339-341:

// 2. make return type: `std::pin::Pin<Box<dyn std::future::Future<Output = #ReturnType> + Send + '_>>`
//    (omit `+ Send` when the `local` crate feature is active or `#[tool(local)]` is used)
let omit_send = cfg!(feature = "local") || attribute.local;

The #[prompt] macro was not updated and unconditionally emits + Send:

rmcp-macros/src/prompt.rs:141:

quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ()> + Send + #lt>> }

rmcp-macros/src/prompt.rs:144:

quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = #ty> + Send + #lt>> }

Reproduction

Enable the local feature and define a server struct holding a !Send type with both tool and prompt handlers:

use rmcp::*;
use std::rc::Rc; // !Send

struct MyServer {
    data: Rc<String>, // !Send
}

#[tool_router]
impl MyServer {
    #[tool(name = "greet")]
    async fn greet(&self) -> Result<CallToolResult, McpError> {
        // ✅ Compiles with `local` feature — future is not required to be Send
        Ok(CallToolResult::text(format!("Hello: {}", self.data)))
    }
}

#[prompt_router]
impl MyServer {
    #[prompt(name = "greeting")]
    async fn greeting(&self) -> Result<GetPromptResult, McpError> {
        // ❌ Fails to compile — future is required to be Send because
        // #[prompt] unconditionally emits `+ Send`
        Ok(GetPromptResult::new(vec![PromptMessage::new_text(
            PromptMessageRole::User,
            format!("Greeting: {}", self.data),
        )]))
    }
}

Speculative Fix

Apply the same omit_send logic from tool.rs to prompt.rs. In rmcp-macros/src/prompt.rs, around line 130-145 where the return type is generated:

// Add this (matching tool.rs:339-341)
let omit_send = cfg!(feature = "local");

// Then change lines 141 and 144 from:
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ()> + Send + #lt>> }
quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = #ty> + Send + #lt>> }

// To:
if omit_send {
    quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ()> + #lt>> }
    quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = #ty> + #lt>> }
} else {
    quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ()> + Send + #lt>> }
    quote! { -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = #ty> + Send + #lt>> }
}

Optionally, also support the per-handler #[prompt(local)] attribute for parity with #[tool(local)].

Use Case

eventkit-rs is an MCP server wrapping Apple's EventKit framework. The underlying Objective-C managers (EKEventStore, EKReminderStore) are !Send. Currently every tool and prompt call must allocate a fresh manager because the local feature can't be used — the #[prompt] handlers reference &self and the macro forces Send on the future. With this fix, managers could be stored directly on the server struct and reused across calls.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is not working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions