Skip to content

Support for #[prompt]-like macro similar to #[tool] #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
blue-orange-yellow opened this issue Mar 29, 2025 · 8 comments
Open

Support for #[prompt]-like macro similar to #[tool] #62

blue-orange-yellow opened this issue Mar 29, 2025 · 8 comments
Labels
enhancement New feature or request

Comments

@blue-orange-yellow
Copy link

No description provided.

@blue-orange-yellow blue-orange-yellow added the enhancement New feature or request label Mar 29, 2025
@4t145
Copy link
Collaborator

4t145 commented Mar 29, 2025

Good idea! This would be on the todo list after the latest revision works!

@blue-orange-yellow
Copy link
Author

@4t145
thank you!
By the way, where do you usually manage our TODOs? Are they tracked somewhere outside of the issues?

@4t145
Copy link
Collaborator

4t145 commented Mar 29, 2025

@blue-orange-yellow We don't have one yet. But in the old rmcp repository, we do have a todo list for every release version.

@jefrydco
Copy link
Contributor

I would love to help tackle this issue. Let's discuss how it looks like. My naive idea is to make it the same as tool one. We'll have these macros

#[prompt(prompt_box)]
#[prompt(name="example_prompt", description="Example Prompt")]
#[prompt(param)]

The usage will looks like this

Sample Usage

pub struct Counter {
  counter: Arc<Mutex<i32>>
}

#[tool(tool_box)]
#[prompt(prompt_box)]
impl Counter {
  pub fn new() -> Self {
    Self {
      counter: Arc::new(Mutex::new(0)),
    }
  }

  #[tool(description = "Increment the counter by 1")]
  async fn increment(&self) -> Result<CallToolResult, McpError> {
    let mut counter = self.counter.lock().await;
    *counter += 1;
    Ok(CallToolResult::success(vec![Content::text(
      counter.to_string(),
    )]))
  }

  #[prompt(name="example_prompt", description="Example description")]
  fn example_prompt(
    &self,
    #[prompt(param)]
    #[schemars(description = "Param description")]
    message: String,
  ) -> Result<GetPromptResult, McpError> {
    let prompt = format!("Example message: '{message}'");

    Ok(GetPromptResult {
      description: None,
      messages: vec![PromptMessage {
        role: PromptMessageRole::User,
        content: PromptMessageContent::text(prompt),
      }],
    })
  }
}

Given that code, it will yield expanded rust code as follows:

Sample Expanded Code

pub struct Counter {
  counter: Arc<Mutex<i32>>
}

impl Counter {
  pub fn new() -> Self {
    Self {
      counter: Arc::new(Mutex::new(0)),
    }
  }

  fn example_prompt_attr() -> rmcp::model::Prompt {
    rmcp::model::Prompt::new(
      name: "example_prompt".into(),
      description: "Example prompt".into(),
    )
  }

  fn example_prompt_get_prompt(
    context: rmcp::handler::server::prompt::GetPromptContext
  ) -> Result<GetPromptResult, McpError> {
    use rmcp::handler::server::prompt::*;
    let (__rmcp_prompt_receiver, context) = <&Self>::from_prompt_get_prompt_context_part(
        context,
    )?;
    Self::example_prompt(__rmcp_prompt_receiver).await.into_get_prompt_prompt_result()
  }

  fn example_prompt(&self, message: String) -> Result<GetPromptResult, McpError> {
    Ok(GetPromptResult {
      description: None,
      messages: vec![PromptMessage {
        role: PromptMessageRole::User,
        content: PromptMessageContent::text(prompt),
      }],
    })
  }

  fn prompt_box() -> &'static ::rmcp::handler::server::prompt::PromptBox<Counter> {
    use ::rmcp::handler::server::prompt::{PromptBox, PromptBoxItem};
    static PROMPT_BOX: std::sync::OnceLock<PromptBox<Counter>> = std::sync::OnceLock::new();
    PROMPT_BOX
      .get_or_init(|| {
          let mut prompt_box = PromptBox::new();
          prompt_box
              .add(
                  PromptBoxItem::new(
                      Counter::example_prompt_attr(),
                      |context| Box::pin(Counter::example_prompt_get_prompt(context)),
                  ),
              );
              prompt_box
      })
  }
}

impl ServerHandler for Counter {
  async fn list_prompts(
    &self,
    _request: PaginatedRequestParam,
    _: RequestContext<RoleServer>,
  ) -> Result<ListPromptsResult, McpError> {
    Ok(::rmcp::model::ListPromptsResult {
      next_cursor: None,
      prompt: Self::prompt_box().list(),
    })
  }

  async fn get_prompt(
    &self,
    get_prompt_request_param: ::rmcp::model::GetPromptRequestParam,
    context: ::rmcp::service::RequestContext<::rmcp::service::RoleServer>,
  ) -> Result<GetPromptResult, McpError> {
    let context = ::rmcp::handler::server::tool::GetPromptContext::new(
      self,
      get_prompt_request_param,
      context,
    );
    Self::prompt_box().get_prompt(context).await
  }
}

What do you think @4t145 @blue-orange-yellow ? We can iterate from it or if there is better approach, would love to hear as well.

@blue-orange-yellow
Copy link
Author

@jefrydco
Thank you for the great idea — I totally agree with you.

By the way, before using this SDK, I was using the following crate:
https://github.com/frozenlib/mcp-attr

It had a similar approach to what you suggested, and I found it quite easy to use.

@4t145
Copy link
Collaborator

4t145 commented Mar 31, 2025

@jefrydco @blue-orange-yellow
My time is limited and I think you can develop prompt macros in parallel. (If you are interested about it.)

And here is a possible composable handler solution, you can review it and give me your suggestion.

@blue-orange-yellow
Copy link
Author

@4t145
I took a look at your implementation.
4t145/rmcp#14 (comment)

Personally, I find this interface to be user-friendly and highly general-purpose.
If I were to implement a counter example using this interface, what would that look like?
A simplified version is totally fine—I'd like to understand this interface more deeply.

@kalvinnchau
Copy link
Contributor

kalvinnchau commented Apr 3, 2025

@jefrydco I think your outlined approach looks great - follows the tool conventions so it should be an easy step for users of the sdk.

Could we follow a similar approach and just have a single top level #[mcp] (or some other name) macro instead of having to put both

#[tool(tool_box)]
#[prompt(prompt_box)]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants