feat: Add prompt support (#351)

* feat: add prompt support with typed argument handling

- Implement #[prompt], #[prompt_router], and #[prompt_handler] macros
- Add automatic JSON schema generation from Rust types for arguments
- Support flexible async handler signatures with automatic adaptation
- Create PromptRouter for efficient prompt dispatch
- Include comprehensive tests and example implementation

This enables MCP servers to provide reusable prompt templates that
LLMs can discover and invoke with strongly-typed parameters, similar
to the existing tool system but optimized for prompt use cases.

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: unify parameter handling between tools and prompts

- Replace Arguments<T> with Parameters<T> for consistent API
- Create shared common module for tool/prompt utilities
- Modernize async handling with futures::future::BoxFuture
- Move cached_schema_for_type to common module for reuse
- Update error types from rmcp::Error to rmcp::ErrorData
- Add comprehensive trait implementations for parameter extraction

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: extract Parameters wrapper to shared module and unify trait usage

- Move Parameters type from common.rs to dedicated wrapper/parameters.rs module
- Unify extractor traits by using FromContextPart for both tools and prompts
- Remove duplicate FromToolCallContextPart and FromPromptContextPart traits
- Add structured output support to Json wrapper with IntoCallToolResult impl
- Improve error messages with more descriptive panic for schema serialization
- Update all imports across codebase to use new module path
- Clean up trailing whitespace and formatting inconsistencies

This consolidates parameter extraction logic and reduces code duplication
between tool and prompt handlers while maintaining backward compatibility.

* chore: remove committed .egg-info directory and update gitignore

* docs: fix documentation formatting
This commit is contained in:
Michael Assaf 2025-08-20 23:22:19 -04:00 • committed by GitHub
commit 9349f5cb26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 3191 additions and 464 deletions

View file

@ -1,7 +1,7 @@
#![allow(dead_code)]
use rmcp::{
ServerHandler,
handler::server::{router::tool::ToolRouter, tool::Parameters},
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_router,
};

View file

@ -1,5 +1,6 @@
use rmcp::{
ErrorData as McpError, handler::server::tool::Parameters, model::*, schemars, tool, tool_router,
ErrorData as McpError, handler::server::wrapper::Parameters, model::*, schemars, tool,
tool_router,
};
use serde::{Deserialize, Serialize};

View file

@ -37,23 +37,26 @@ impl ClientHandler for MyClient {
}
}
impl Default for MyServer {
fn default() -> Self {
Self::new()
}
}
pub struct MyServer {
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl MyServer {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
impl Default for MyServer {
fn default() -> Self {
Self::new()
}
}
#[tool_router]
impl MyServer {
#[tool]
pub async fn some_progress(
meta: Meta,

View file

@ -0,0 +1,161 @@
//cargo test --test test_prompt_handler --features "client server"
// Tests for verifying that the #[prompt_handler] macro correctly generates
// the ServerHandler trait implementation methods.
#![allow(dead_code)]
use rmcp::{
RoleServer, ServerHandler,
handler::server::router::prompt::PromptRouter,
model::{GetPromptRequestParam, GetPromptResult, ListPromptsResult, PaginatedRequestParam},
prompt_handler,
service::RequestContext,
};
#[derive(Debug, Clone)]
pub struct TestPromptServer {
prompt_router: PromptRouter<Self>,
}
impl Default for TestPromptServer {
fn default() -> Self {
Self::new()
}
}
impl TestPromptServer {
pub fn new() -> Self {
Self {
prompt_router: PromptRouter::new(),
}
}
}
#[prompt_handler]
impl ServerHandler for TestPromptServer {}
#[derive(Debug, Clone)]
pub struct CustomRouterServer {
custom_router: PromptRouter<Self>,
}
impl Default for CustomRouterServer {
fn default() -> Self {
Self::new()
}
}
impl CustomRouterServer {
pub fn new() -> Self {
Self {
custom_router: PromptRouter::new(),
}
}
pub fn get_custom_router(&self) -> &PromptRouter<Self> {
&self.custom_router
}
}
#[prompt_handler(router = self.custom_router)]
impl ServerHandler for CustomRouterServer {}
#[derive(Debug, Clone)]
pub struct GenericPromptServer<T: Send + Sync + 'static> {
prompt_router: PromptRouter<Self>,
_marker: std::marker::PhantomData<T>,
}
impl<T: Send + Sync + 'static> Default for GenericPromptServer<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Send + Sync + 'static> GenericPromptServer<T> {
pub fn new() -> Self {
Self {
prompt_router: PromptRouter::new(),
_marker: std::marker::PhantomData,
}
}
}
#[prompt_handler]
impl<T: Send + Sync + 'static> ServerHandler for GenericPromptServer<T> {}
#[test]
fn test_prompt_handler_basic() {
let server = TestPromptServer::new();
// Test that the server implements ServerHandler
fn assert_server_handler<T: ServerHandler>(_: &T) {}
assert_server_handler(&server);
// Test that the prompt router is accessible
assert_eq!(server.prompt_router.list_all().len(), 0);
}
#[test]
fn test_prompt_handler_custom_router() {
let server = CustomRouterServer::new();
// Test that the server implements ServerHandler
fn assert_server_handler<T: ServerHandler>(_: &T) {}
assert_server_handler(&server);
// Test that the custom router is used
assert_eq!(server.custom_router.list_all().len(), 0);
}
#[test]
fn test_prompt_handler_with_generics() {
let server = GenericPromptServer::<String>::new();
// Test that generic server implements ServerHandler
fn assert_server_handler<T: ServerHandler>(_: &T) {}
assert_server_handler(&server);
// Test with a different generic type
let server2 = GenericPromptServer::<i32>::new();
assert_server_handler(&server2);
}
#[test]
fn test_prompt_handler_trait_implementation() {
// This test verifies that the prompt_handler macro generates proper ServerHandler implementation
// The actual method signatures are tested through the ServerHandler trait bound
fn compile_time_check<T: ServerHandler>() {}
compile_time_check::<TestPromptServer>();
compile_time_check::<CustomRouterServer>();
compile_time_check::<GenericPromptServer<String>>();
}
// Test that the macro works with different server configurations
mod nested {
use super::*;
#[derive(Debug, Clone)]
pub struct NestedServer {
prompt_router: PromptRouter<Self>,
}
impl NestedServer {
pub fn new() -> Self {
Self {
prompt_router: PromptRouter::new(),
}
}
}
#[prompt_handler]
impl ServerHandler for NestedServer {}
#[test]
fn test_nested_prompt_handler() {
let server = NestedServer::new();
// Verify it implements ServerHandler
fn assert_server_handler<T: ServerHandler>(_: &T) {}
assert_server_handler(&server);
}
}

View file

@ -0,0 +1,291 @@
//cargo test --test test_prompt_macro_annotations --features "client server"
#![allow(dead_code)]
use rmcp::{
ServerHandler,
handler::server::wrapper::Parameters,
model::{GetPromptResult, Prompt, PromptMessage, PromptMessageRole},
prompt,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
struct TestServer;
impl ServerHandler for TestServer {}
#[derive(Serialize, Deserialize, JsonSchema)]
struct TestArgs {
/// The input text to process
input: String,
/// Optional configuration
#[serde(skip_serializing_if = "Option::is_none")]
config: Option<String>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
struct ComplexArgs {
/// Required field
required_field: String,
/// Optional string field
#[schemars(description = "An optional string parameter")]
optional_string: Option<String>,
/// Optional number field
optional_number: Option<i64>,
/// Array field
items: Vec<String>,
}
// Test basic prompt attribute generation
#[prompt]
async fn basic_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Basic response",
)]
}
// Test prompt with custom name
#[prompt(name = "custom_name")]
async fn named_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Named response",
)]
}
// Test prompt with custom description
#[prompt(description = "This is a custom description")]
async fn described_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Described response",
)]
}
// Test prompt with both name and description
#[prompt(name = "full_custom", description = "Fully customized prompt")]
async fn fully_custom_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Fully custom response",
)]
}
// Test prompt with doc comments
/// This is a doc comment description
/// that spans multiple lines
#[prompt]
async fn doc_comment_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Doc comment response",
)]
}
// Test prompt with doc comments and explicit description (explicit wins)
/// This is a doc comment
#[prompt(description = "This overrides the doc comment")]
async fn override_doc_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Override response",
)]
}
// Test prompt with arguments
#[prompt]
async fn args_prompt(_server: &TestServer, _args: Parameters<TestArgs>) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Args response",
)]
}
// Test prompt with complex arguments
#[prompt]
async fn complex_args_prompt(
_server: &TestServer,
_args: Parameters<ComplexArgs>,
) -> GetPromptResult {
GetPromptResult {
description: Some("Complex args result".to_string()),
messages: vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Complex response",
)],
}
}
// Test sync prompt
#[prompt]
fn sync_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Sync response",
)]
}
#[test]
fn test_basic_prompt_attr() {
let attr = basic_prompt_prompt_attr();
assert_eq!(attr.name, "basic_prompt");
assert_eq!(attr.description, None);
assert!(attr.arguments.is_none());
}
#[test]
fn test_named_prompt_attr() {
let attr = named_prompt_prompt_attr();
assert_eq!(attr.name, "custom_name");
assert_eq!(attr.description, None);
assert!(attr.arguments.is_none());
}
#[test]
fn test_described_prompt_attr() {
let attr = described_prompt_prompt_attr();
assert_eq!(attr.name, "described_prompt");
assert_eq!(
attr.description.as_deref(),
Some("This is a custom description")
);
assert!(attr.arguments.is_none());
}
#[test]
fn test_fully_custom_prompt_attr() {
let attr = fully_custom_prompt_prompt_attr();
assert_eq!(attr.name, "full_custom");
assert_eq!(attr.description.as_deref(), Some("Fully customized prompt"));
assert!(attr.arguments.is_none());
}
#[test]
fn test_doc_comment_prompt_attr() {
let attr = doc_comment_prompt_prompt_attr();
assert_eq!(attr.name, "doc_comment_prompt");
assert!(attr.description.is_some());
let desc = attr.description.unwrap();
assert!(desc.contains("This is a doc comment description"));
assert!(desc.contains("that spans multiple lines"));
}
#[test]
fn test_override_doc_prompt_attr() {
let attr = override_doc_prompt_prompt_attr();
assert_eq!(attr.name, "override_doc_prompt");
assert_eq!(
attr.description.as_deref(),
Some("This overrides the doc comment")
);
}
#[test]
fn test_args_prompt_attr() {
let attr = args_prompt_prompt_attr();
assert_eq!(attr.name, "args_prompt");
let args = attr.arguments.as_ref().unwrap();
assert_eq!(args.len(), 2);
// Check input field
let input_arg = args.iter().find(|a| a.name == "input").unwrap();
assert_eq!(input_arg.required, Some(true));
assert_eq!(
input_arg.description.as_deref(),
Some("The input text to process")
);
// Check config field
let config_arg = args.iter().find(|a| a.name == "config").unwrap();
assert_eq!(config_arg.required, Some(false));
assert_eq!(
config_arg.description.as_deref(),
Some("Optional configuration")
);
}
#[test]
fn test_complex_args_prompt_attr() {
let attr = complex_args_prompt_prompt_attr();
assert_eq!(attr.name, "complex_args_prompt");
let args = attr.arguments.as_ref().unwrap();
assert_eq!(args.len(), 4);
// Check required_field
let required_arg = args.iter().find(|a| a.name == "required_field").unwrap();
assert_eq!(required_arg.required, Some(true));
assert_eq!(required_arg.description.as_deref(), Some("Required field"));
// Check optional_string
let optional_string_arg = args.iter().find(|a| a.name == "optional_string").unwrap();
assert_eq!(optional_string_arg.required, Some(false));
assert_eq!(
optional_string_arg.description.as_deref(),
Some("An optional string parameter")
);
// Check optional_number
let optional_number_arg = args.iter().find(|a| a.name == "optional_number").unwrap();
assert_eq!(optional_number_arg.required, Some(false));
assert_eq!(
optional_number_arg.description.as_deref(),
Some("Optional number field")
);
// Check items
let items_arg = args.iter().find(|a| a.name == "items").unwrap();
assert_eq!(items_arg.required, Some(true));
assert_eq!(items_arg.description.as_deref(), Some("Array field"));
}
#[test]
fn test_sync_prompt_attr() {
let attr = sync_prompt_prompt_attr();
assert_eq!(attr.name, "sync_prompt");
assert!(attr.arguments.is_none());
}
#[test]
fn test_prompt_attr_function_type() {
// Test that the generated function returns the correct type
fn assert_prompt_attr_fn(_: impl Fn() -> Prompt) {}
assert_prompt_attr_fn(basic_prompt_prompt_attr);
assert_prompt_attr_fn(named_prompt_prompt_attr);
assert_prompt_attr_fn(described_prompt_prompt_attr);
assert_prompt_attr_fn(fully_custom_prompt_prompt_attr);
assert_prompt_attr_fn(doc_comment_prompt_prompt_attr);
assert_prompt_attr_fn(override_doc_prompt_prompt_attr);
assert_prompt_attr_fn(args_prompt_prompt_attr);
assert_prompt_attr_fn(complex_args_prompt_prompt_attr);
assert_prompt_attr_fn(sync_prompt_prompt_attr);
}
// Test generic prompts
#[derive(Debug, Clone)]
struct GenericServer<T: Send + Sync + 'static> {
_marker: std::marker::PhantomData<T>,
}
impl<T: Send + Sync + 'static> ServerHandler for GenericServer<T> {}
#[prompt]
async fn generic_prompt<T: Send + Sync + 'static>(
_server: &GenericServer<T>,
) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Generic response",
)]
}
#[test]
fn test_generic_prompt_attr() {
let attr = generic_prompt_prompt_attr();
assert_eq!(attr.name, "generic_prompt");
assert!(attr.arguments.is_none());
}

View file

@ -0,0 +1,383 @@
//cargo test --test test_prompt_macros --features "client server"
#![allow(dead_code)]
use std::sync::Arc;
use rmcp::{
ClientHandler, RoleServer, ServerHandler, ServiceExt,
handler::server::{router::prompt::PromptRouter, wrapper::Parameters},
model::{
ClientInfo, GetPromptRequestParam, GetPromptResult, ListPromptsResult,
PaginatedRequestParam, PromptMessage, PromptMessageRole,
},
prompt, prompt_handler, prompt_router,
service::RequestContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct CodeReviewRequest {
pub file_path: String,
pub language: String,
}
#[prompt_handler(router = self.prompt_router)]
impl ServerHandler for Server {}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Server {
prompt_router: PromptRouter<Self>,
}
impl Default for Server {
fn default() -> Self {
Self::new()
}
}
#[prompt_router]
impl Server {
pub fn new() -> Self {
Self {
prompt_router: Self::prompt_router(),
}
}
/// This prompt is used to review code for best practices.
#[prompt(
name = "code-review",
description = "Review code for best practices and issues."
)]
pub async fn code_review(&self, params: Parameters<CodeReviewRequest>) -> Vec<PromptMessage> {
vec![
PromptMessage::new_text(
PromptMessageRole::User,
format!(
"Please review the {} code in: {}",
params.0.language, params.0.file_path
),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
"I'll review this code for best practices and potential issues.".to_string(),
),
]
}
#[prompt]
async fn empty_param(&self) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"This is a prompt with no parameters.".to_string(),
)]
}
}
// define generic service trait
pub trait DataService: Send + Sync + 'static {
fn get_context(&self) -> String;
}
// mock service for test
#[derive(Clone)]
struct MockDataService;
impl DataService for MockDataService {
fn get_context(&self) -> String {
"mock context data".to_string()
}
}
// define generic server
#[derive(Debug, Clone)]
pub struct GenericServer<DS: DataService> {
data_service: Arc<DS>,
prompt_router: PromptRouter<Self>,
}
#[prompt_router]
impl<DS: DataService> GenericServer<DS> {
pub fn new(data_service: DS) -> Self {
Self {
data_service: Arc::new(data_service),
prompt_router: Self::prompt_router(),
}
}
#[prompt(description = "Get contextual help from the service")]
async fn get_help(&self) -> GetPromptResult {
let context = self.data_service.get_context();
GetPromptResult {
description: Some("Contextual help based on service data".to_string()),
messages: vec![
PromptMessage::new_text(
PromptMessageRole::User,
"I need help with the current context.".to_string(),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
format!(
"Based on the context '{}', here's how I can help...",
context
),
),
],
}
}
}
#[prompt_handler]
impl<DS: DataService> ServerHandler for GenericServer<DS> {}
#[tokio::test]
async fn test_prompt_macros() {
let server = Server::new();
let _attr = Server::code_review_prompt_attr();
let _code_review_prompt_attr_fn = Server::code_review_prompt_attr;
let _code_review_fn = Server::code_review;
let result = server
.code_review(Parameters(CodeReviewRequest {
file_path: "/src/main.rs".into(),
language: "rust".into(),
}))
.await;
assert_eq!(result.len(), 2);
assert_eq!(result[0].role, PromptMessageRole::User);
assert_eq!(result[1].role, PromptMessageRole::Assistant);
}
#[tokio::test]
async fn test_prompt_macros_with_empty_param() {
let _attr = Server::empty_param_prompt_attr();
println!("{_attr:?}");
assert!(
_attr.arguments.is_none(),
"Empty param prompt should have no arguments"
);
}
#[tokio::test]
async fn test_prompt_macros_with_generics() {
let mock_service = MockDataService;
let server = GenericServer::new(mock_service);
let _attr = GenericServer::<MockDataService>::get_help_prompt_attr();
let _get_help_call_fn = GenericServer::<MockDataService>::get_help;
let _get_help_fn = GenericServer::<MockDataService>::get_help;
let result = server.get_help().await;
assert!(result.description.is_some());
assert_eq!(result.messages.len(), 2);
match &result.messages[1].content {
rmcp::model::PromptMessageContent::Text { text } => {
assert!(text.contains("mock context data"));
}
_ => panic!("Expected text content"),
}
}
#[tokio::test]
async fn test_prompt_macros_with_optional_param() {
let _attr = Server::code_review_prompt_attr();
let arguments = _attr.arguments.as_ref().unwrap();
// Check that we have the expected number of arguments
assert_eq!(arguments.len(), 2);
// Verify file_path is required
let file_path_arg = arguments.iter().find(|a| a.name == "file_path").unwrap();
assert_eq!(file_path_arg.required, Some(true));
// Verify language is required
let language_arg = arguments.iter().find(|a| a.name == "language").unwrap();
assert_eq!(language_arg.required, Some(true));
}
impl CodeReviewRequest {}
// Struct defined for testing optional field schema generation
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct OptionalFieldTestSchema {
#[schemars(description = "An optional description field")]
pub description: Option<String>,
}
// Struct defined for testing optional i64 field schema generation and null handling
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct OptionalI64TestSchema {
#[schemars(description = "An optional i64 field")]
pub count: Option<i64>,
pub mandatory_field: String, // Added to ensure non-empty object schema
}
// Dummy struct to host the test prompt method
#[derive(Debug, Clone)]
pub struct OptionalSchemaTester {
prompt_router: PromptRouter<Self>,
}
impl Default for OptionalSchemaTester {
fn default() -> Self {
Self::new()
}
}
impl OptionalSchemaTester {
pub fn new() -> Self {
Self {
prompt_router: Self::prompt_router(),
}
}
}
#[prompt_router]
impl OptionalSchemaTester {
// Dummy prompt function using the test schema as an aggregated parameter
#[prompt(description = "A prompt to test optional schema generation")]
async fn test_optional(&self, _req: Parameters<OptionalFieldTestSchema>) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Testing optional fields".to_string(),
)]
}
// Prompt function to test optional i64 handling
#[prompt(description = "A prompt to test optional i64 schema generation")]
async fn test_optional_i64(
&self,
Parameters(req): Parameters<OptionalI64TestSchema>,
) -> GetPromptResult {
let message = match req.count {
Some(c) => format!("Received count: {}", c),
None => "Received null count".to_string(),
};
GetPromptResult {
description: Some("Test result for optional i64".to_string()),
messages: vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
message,
)],
}
}
}
#[prompt_handler]
// Implement ServerHandler to route prompt calls for OptionalSchemaTester
impl ServerHandler for OptionalSchemaTester {}
#[test]
fn test_optional_field_schema_generation_via_macro() {
// tests https://github.com/modelcontextprotocol/rust-sdk/issues/135
// Get the attributes generated by the #[prompt] macro helper
let prompt_attr = OptionalSchemaTester::test_optional_prompt_attr();
// Print the actual generated schema for debugging
println!(
"Actual arguments generated by macro: {:#?}",
prompt_attr.arguments
);
// Verify the schema generated for the aggregated OptionalFieldTestSchema
let arguments = prompt_attr.arguments.expect("Should have arguments");
// Check that we have an argument for the optional description field
let description_arg = arguments
.iter()
.find(|arg| arg.name == "description")
.expect("Should have description argument");
// Assert that optional fields are marked as not required
assert_eq!(
description_arg.required,
Some(false),
"Optional fields should be marked as not required"
);
// Check the description is correct
assert_eq!(
description_arg.description.as_deref(),
Some("An optional description field")
);
}
// Define a dummy client handler
#[derive(Debug, Clone, Default)]
struct DummyClientHandler {}
impl ClientHandler for DummyClientHandler {
fn get_info(&self) -> ClientInfo {
ClientInfo::default()
}
}
#[tokio::test]
async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
let (server_transport, client_transport) = tokio::io::duplex(4096);
// Server setup
let server = OptionalSchemaTester::new();
let server_handle = tokio::spawn(async move {
server.serve(server_transport).await?.waiting().await?;
anyhow::Ok(())
});
// Create a simple client handler that just forwards prompt calls
let client_handler = DummyClientHandler::default();
let client = client_handler.serve(client_transport).await?;
// Test null case
let result = client
.get_prompt(GetPromptRequestParam {
name: "test_optional_i64".into(),
arguments: Some(
serde_json::json!({
"count": null,
"mandatory_field": "test_null"
})
.as_object()
.unwrap()
.clone(),
),
})
.await?;
let result_text = match &result.messages.first().unwrap().content {
rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
_ => panic!("Expected text content"),
};
assert_eq!(
result_text, "Received null count",
"Null case should return expected message"
);
// Test Some case
let some_result = client
.get_prompt(GetPromptRequestParam {
name: "test_optional_i64".into(),
arguments: Some(
serde_json::json!({
"count": 42,
"mandatory_field": "test_some"
})
.as_object()
.unwrap()
.clone(),
),
})
.await?;
let some_result_text = match &some_result.messages.first().unwrap().content {
rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
_ => panic!("Expected text content"),
};
assert_eq!(
some_result_text, "Received count: 42",
"Some case should return expected message"
);
client.cancel().await?;
server_handle.await??;
Ok(())
}

View file

@ -0,0 +1,105 @@
use std::collections::HashMap;
use futures::future::BoxFuture;
use rmcp::{
ServerHandler,
handler::server::wrapper::Parameters,
model::{GetPromptResult, PromptMessage, PromptMessageRole},
};
#[derive(Debug, Default)]
pub struct TestHandler<T: 'static = ()> {
pub _marker: std::marker::PhantomData<fn(*const T)>,
}
impl<T: 'static> ServerHandler for TestHandler<T> {}
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub fields: HashMap<String, String>,
}
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
pub struct Sum {
pub a: i32,
pub b: i32,
}
#[rmcp::prompt_router(router = "test_router")]
impl<T> TestHandler<T> {
#[rmcp::prompt]
async fn async_method(
&self,
Parameters(Request { fields }): Parameters<Request>,
) -> Vec<PromptMessage> {
drop(fields);
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Async method response",
)]
}
#[rmcp::prompt]
fn sync_method(
&self,
Parameters(Request { fields }): Parameters<Request>,
) -> Vec<PromptMessage> {
drop(fields);
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Sync method response",
)]
}
}
#[rmcp::prompt]
async fn async_function(Parameters(Request { fields }): Parameters<Request>) -> Vec<PromptMessage> {
drop(fields);
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Async function response",
)]
}
#[rmcp::prompt]
fn async_function2<T>(_callee: &TestHandler<T>) -> BoxFuture<'_, GetPromptResult> {
Box::pin(async move {
GetPromptResult {
description: Some("Async function 2".to_string()),
messages: vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Async function 2 response",
)],
}
})
}
#[test]
fn test_prompt_router() {
let test_prompt_router = TestHandler::<()>::test_router()
.with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn(
async_function_prompt_attr(),
|mut context| {
Box::pin(async move {
use rmcp::handler::server::{
common::FromContextPart, prompt::IntoGetPromptResult,
};
let params = Parameters::<Request>::from_context_part(&mut context)?;
let result = async_function(params).await;
result.into_get_prompt_result()
})
},
))
.with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn(
async_function2_prompt_attr(),
|context| {
Box::pin(async move {
use rmcp::handler::server::prompt::IntoGetPromptResult;
let result = async_function2(context.server).await;
result.into_get_prompt_result()
})
},
));
let prompts = test_prompt_router.list_all();
assert_eq!(prompts.len(), 4);
}

View file

@ -1,7 +1,7 @@
//cargo test --test test_structured_output --features "client server macros"
use rmcp::{
Json, ServerHandler,
handler::server::{router::tool::ToolRouter, tool::Parameters},
handler::server::{router::tool::ToolRouter, tool::IntoCallToolResult, wrapper::Parameters},
model::{CallToolResult, Content, Tool},
tool, tool_handler, tool_router,
};
@ -266,12 +266,12 @@ async fn test_output_schema_requires_structured_content() {
assert!(calculate_tool.output_schema.is_some());
// Directly call the tool and verify its result structure
let params = rmcp::handler::server::tool::Parameters(CalculationRequest { a: 5, b: 3 });
let params = Parameters(CalculationRequest { a: 5, b: 3 });
let result = server.calculate(params).await.unwrap();
// Convert the Json<CalculationResult> to CallToolResult
let call_result: Result<CallToolResult, rmcp::ErrorData> =
rmcp::handler::server::tool::IntoCallToolResult::into_call_tool_result(result);
IntoCallToolResult::into_call_tool_result(result);
assert!(call_result.is_ok());
let call_result = call_result.unwrap();

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use rmcp::{
ClientHandler, ServerHandler, ServiceExt,
handler::server::{router::tool::ToolRouter, tool::Parameters},
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{CallToolRequestParam, ClientInfo},
tool, tool_handler, tool_router,
};
@ -26,6 +26,14 @@ pub struct Server {
tool_router: ToolRouter<Self>,
}
impl Server {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
impl Default for Server {
fn default() -> Self {
Self::new()
@ -34,12 +42,6 @@ impl Default for Server {
#[tool_router(router = tool_router)]
impl Server {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.")]
pub async fn get_weather(&self, city: Parameters<GetWeatherRequest>) -> String {

View file

@ -3,10 +3,7 @@ use std::collections::HashMap;
use futures::future::BoxFuture;
use rmcp::{
ServerHandler,
handler::server::{
router::tool::ToolRouter,
tool::{CallToolHandler, Parameters},
},
handler::server::{router::tool::ToolRouter, tool::CallToolHandler, wrapper::Parameters},
};
#[derive(Debug, Default)]

View file

@ -1 +1,22 @@
*.lock
# Lock files
*.lock
# Python build artifacts
*.egg-info/
build/
dist/
__pycache__/
*.py[cod]
*$py.class
# Virtual environments
venv/
env/
.venv/
.env/
# IDE
.vscode/
.idea/
*.swp
*.swo