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:
parent
c0452f93a4
commit
9349f5cb26
6 changed files with 3191 additions and 464 deletions
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
handler::server::{router::tool::ToolRouter, tool::Parameters, wrapper::Json},
|
||||
handler::server::{
|
||||
router::tool::ToolRouter,
|
||||
wrapper::{Json, Parameters},
|
||||
},
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ use std::sync::Arc;
|
|||
|
||||
use rmcp::{
|
||||
ErrorData as McpError, RoleServer, ServerHandler,
|
||||
handler::server::{router::tool::ToolRouter, tool::Parameters},
|
||||
handler::server::{
|
||||
router::{prompt::PromptRouter, tool::ToolRouter},
|
||||
wrapper::Parameters,
|
||||
},
|
||||
model::*,
|
||||
schemars,
|
||||
prompt, prompt_handler, prompt_router, schemars,
|
||||
service::RequestContext,
|
||||
tool, tool_handler, tool_router,
|
||||
};
|
||||
|
|
@ -18,10 +21,26 @@ pub struct StructRequest {
|
|||
pub b: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct ExamplePromptArgs {
|
||||
/// A message to put in the prompt
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct CounterAnalysisArgs {
|
||||
/// The target value you're trying to reach
|
||||
pub goal: i32,
|
||||
/// Preferred strategy: 'fast' or 'careful'
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub strategy: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Counter {
|
||||
counter: Arc<Mutex<i32>>,
|
||||
tool_router: ToolRouter<Counter>,
|
||||
prompt_router: PromptRouter<Counter>,
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
|
|
@ -31,6 +50,7 @@ impl Counter {
|
|||
Self {
|
||||
counter: Arc::new(Mutex::new(0)),
|
||||
tool_router: Self::tool_router(),
|
||||
prompt_router: Self::prompt_router(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +106,63 @@ impl Counter {
|
|||
)]))
|
||||
}
|
||||
}
|
||||
|
||||
#[prompt_router]
|
||||
impl Counter {
|
||||
/// This is an example prompt that takes one required argument, message
|
||||
#[prompt(name = "example_prompt")]
|
||||
async fn example_prompt(
|
||||
&self,
|
||||
Parameters(args): Parameters<ExamplePromptArgs>,
|
||||
_ctx: RequestContext<RoleServer>,
|
||||
) -> Result<Vec<PromptMessage>, McpError> {
|
||||
let prompt = format!(
|
||||
"This is an example prompt with your message here: '{}'",
|
||||
args.message
|
||||
);
|
||||
Ok(vec![PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: PromptMessageContent::text(prompt),
|
||||
}])
|
||||
}
|
||||
|
||||
/// Analyze the current counter value and suggest next steps
|
||||
#[prompt(name = "counter_analysis")]
|
||||
async fn counter_analysis(
|
||||
&self,
|
||||
Parameters(args): Parameters<CounterAnalysisArgs>,
|
||||
_ctx: RequestContext<RoleServer>,
|
||||
) -> Result<GetPromptResult, McpError> {
|
||||
let strategy = args.strategy.unwrap_or_else(|| "careful".to_string());
|
||||
let current_value = *self.counter.lock().await;
|
||||
let difference = args.goal - current_value;
|
||||
|
||||
let messages = vec![
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
"I'll analyze the counter situation and suggest the best approach.",
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
format!(
|
||||
"Current counter value: {}\nGoal value: {}\nDifference: {}\nStrategy preference: {}\n\nPlease analyze the situation and suggest the best approach to reach the goal.",
|
||||
current_value, args.goal, difference, strategy
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
Ok(GetPromptResult {
|
||||
description: Some(format!(
|
||||
"Counter analysis for reaching {} from {}",
|
||||
args.goal, current_value
|
||||
)),
|
||||
messages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
#[prompt_handler]
|
||||
impl ServerHandler for Counter {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
|
|
@ -97,7 +173,7 @@ impl ServerHandler for Counter {
|
|||
.enable_tools()
|
||||
.build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some("This server provides a counter tool that can increment and decrement values. The counter starts at 0 and can be modified using the 'increment' and 'decrement' tools. Use 'get_value' to check the current count.".to_string()),
|
||||
instructions: Some("This server provides counter tools and prompts. Tools: increment, decrement, get_value, say_hello, echo, sum. Prompts: example_prompt (takes a message), counter_analysis (analyzes counter state with a goal).".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,52 +218,6 @@ impl ServerHandler for Counter {
|
|||
}
|
||||
}
|
||||
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
_request: Option<PaginatedRequestParam>,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ListPromptsResult, McpError> {
|
||||
Ok(ListPromptsResult {
|
||||
next_cursor: None,
|
||||
prompts: vec![Prompt::new(
|
||||
"example_prompt",
|
||||
Some("This is an example prompt that takes one required argument, message"),
|
||||
Some(vec![PromptArgument {
|
||||
name: "message".to_string(),
|
||||
description: Some("A message to put in the prompt".to_string()),
|
||||
required: Some(true),
|
||||
}]),
|
||||
)],
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_prompt(
|
||||
&self,
|
||||
GetPromptRequestParam { name, arguments }: GetPromptRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<GetPromptResult, McpError> {
|
||||
match name.as_str() {
|
||||
"example_prompt" => {
|
||||
let message = arguments
|
||||
.and_then(|json| json.get("message")?.as_str().map(|s| s.to_string()))
|
||||
.ok_or_else(|| {
|
||||
McpError::invalid_params("No message provided to example_prompt", None)
|
||||
})?;
|
||||
|
||||
let prompt =
|
||||
format!("This is an example prompt with your message here: '{message}'");
|
||||
Ok(GetPromptResult {
|
||||
description: None,
|
||||
messages: vec![PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: PromptMessageContent::text(prompt),
|
||||
}],
|
||||
})
|
||||
}
|
||||
_ => Err(McpError::invalid_params("prompt not found", None)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_resource_templates(
|
||||
&self,
|
||||
_request: Option<PaginatedRequestParam>,
|
||||
|
|
@ -212,3 +242,76 @@ impl ServerHandler for Counter {
|
|||
Ok(self.get_info())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_prompt_attributes_generated() {
|
||||
// Verify that the prompt macros generate the expected attributes
|
||||
let example_attr = Counter::example_prompt_prompt_attr();
|
||||
assert_eq!(example_attr.name, "example_prompt");
|
||||
assert!(example_attr.description.is_some());
|
||||
assert!(example_attr.arguments.is_some());
|
||||
|
||||
let args = example_attr.arguments.unwrap();
|
||||
assert_eq!(args.len(), 1);
|
||||
assert_eq!(args[0].name, "message");
|
||||
assert_eq!(args[0].required, Some(true));
|
||||
|
||||
let analysis_attr = Counter::counter_analysis_prompt_attr();
|
||||
assert_eq!(analysis_attr.name, "counter_analysis");
|
||||
assert!(analysis_attr.description.is_some());
|
||||
assert!(analysis_attr.arguments.is_some());
|
||||
|
||||
let args = analysis_attr.arguments.unwrap();
|
||||
assert_eq!(args.len(), 2);
|
||||
assert_eq!(args[0].name, "goal");
|
||||
assert_eq!(args[0].required, Some(true));
|
||||
assert_eq!(args[1].name, "strategy");
|
||||
assert_eq!(args[1].required, Some(false));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_prompt_router_has_routes() {
|
||||
let router = Counter::prompt_router();
|
||||
assert!(router.has_route("example_prompt"));
|
||||
assert!(router.has_route("counter_analysis"));
|
||||
|
||||
let prompts = router.list_all();
|
||||
assert_eq!(prompts.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_example_prompt_execution() {
|
||||
let counter = Counter::new();
|
||||
let context = rmcp::handler::server::prompt::PromptContext::new(
|
||||
&counter,
|
||||
"example_prompt".to_string(),
|
||||
Some({
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert(
|
||||
"message".to_string(),
|
||||
serde_json::Value::String("Test message".to_string()),
|
||||
);
|
||||
map
|
||||
}),
|
||||
RequestContext {
|
||||
meta: Default::default(),
|
||||
ct: tokio_util::sync::CancellationToken::new(),
|
||||
id: rmcp::model::NumberOrString::String("test-1".to_string()),
|
||||
peer: Default::default(),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let router = Counter::prompt_router();
|
||||
let result = router.get_prompt(context).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let prompt_result = result.unwrap();
|
||||
assert_eq!(prompt_result.messages.len(), 1);
|
||||
assert_eq!(prompt_result.messages[0].role, PromptMessageRole::User);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
handler::server::{router::tool::ToolRouter, tool::Parameters},
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::sync::Arc;
|
|||
use anyhow::Result;
|
||||
use rmcp::{
|
||||
ErrorData as McpError, ServerHandler, ServiceExt, elicit_safe,
|
||||
handler::server::{router::tool::ToolRouter, tool::Parameters},
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::*,
|
||||
schemars::JsonSchema,
|
||||
service::{RequestContext, RoleServer},
|
||||
|
|
@ -42,6 +42,15 @@ pub struct ElicitationServer {
|
|||
tool_router: ToolRouter<ElicitationServer>,
|
||||
}
|
||||
|
||||
impl ElicitationServer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
user_name: Arc::new(Mutex::new(None)),
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ElicitationServer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
|
@ -50,13 +59,6 @@ impl Default for ElicitationServer {
|
|||
|
||||
#[tool_router]
|
||||
impl ElicitationServer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
user_name: Arc::new(Mutex::new(None)),
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Greet user with name collection")]
|
||||
async fn greet_user(
|
||||
&self,
|
||||
|
|
|
|||
423
examples/servers/src/prompt_stdio.rs
Normal file
423
examples/servers/src/prompt_stdio.rs
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
//! MCP Server demonstrating prompt functionality
|
||||
//!
|
||||
//! This example shows how to use prompt macros to create an MCP server
|
||||
//! that provides various types of prompts to LLM clients.
|
||||
//!
|
||||
//! Run with MCP Inspector:
|
||||
//! ```bash
|
||||
//! npx @modelcontextprotocol/inspector cargo run -p mcp-server-examples --example servers_prompt_stdio
|
||||
//! ```
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use rmcp::{
|
||||
ErrorData as McpError, RoleServer, ServerHandler, ServiceExt,
|
||||
handler::server::{router::prompt::PromptRouter, wrapper::Parameters},
|
||||
model::*,
|
||||
prompt, prompt_handler, prompt_router,
|
||||
schemars::JsonSchema,
|
||||
service::RequestContext,
|
||||
transport::stdio,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing_subscriber::{self, EnvFilter};
|
||||
|
||||
/// Arguments for the code review prompt
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(description = "Code review parameters")]
|
||||
pub struct CodeReviewArgs {
|
||||
#[schemars(description = "Programming language of the code")]
|
||||
pub language: String,
|
||||
#[schemars(description = "Path to the file or code snippet")]
|
||||
pub file_path: String,
|
||||
#[schemars(description = "Focus areas for the review")]
|
||||
pub focus_areas: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Arguments for the data analysis prompt
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(description = "Data analysis parameters")]
|
||||
pub struct DataAnalysisArgs {
|
||||
#[schemars(description = "Type of data: 'csv', 'json', 'logs', etc.")]
|
||||
pub data_type: String,
|
||||
#[schemars(description = "What kind of analysis to perform")]
|
||||
pub analysis_type: String,
|
||||
#[schemars(description = "Additional context about the data")]
|
||||
pub context: Option<String>,
|
||||
}
|
||||
|
||||
/// Arguments for the writing assistant prompt
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(description = "Writing assistant parameters")]
|
||||
pub struct WritingAssistantArgs {
|
||||
#[schemars(description = "Type of content: 'email', 'documentation', 'blog', etc.")]
|
||||
pub content_type: String,
|
||||
#[schemars(description = "Target audience")]
|
||||
pub audience: String,
|
||||
#[schemars(description = "Writing tone: 'formal', 'casual', 'technical', etc.")]
|
||||
pub tone: Option<String>,
|
||||
#[schemars(description = "Key points to cover")]
|
||||
pub key_points: Vec<String>,
|
||||
}
|
||||
|
||||
/// Arguments for the debug assistant prompt
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(description = "Debug assistant parameters")]
|
||||
pub struct DebugAssistantArgs {
|
||||
#[schemars(description = "Error message or symptom")]
|
||||
pub error_message: String,
|
||||
#[schemars(description = "Technology stack involved")]
|
||||
pub stack: Vec<String>,
|
||||
#[schemars(description = "Steps already tried")]
|
||||
pub tried_solutions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Simple prompt server demonstrating various prompt patterns
|
||||
#[derive(Clone)]
|
||||
pub struct PromptServer {
|
||||
/// Stores user preferences that can be used in prompts
|
||||
user_preferences: Arc<RwLock<UserPreferences>>,
|
||||
prompt_router: PromptRouter<PromptServer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UserPreferences {
|
||||
preferred_language: String,
|
||||
expertise_level: String,
|
||||
}
|
||||
|
||||
impl Default for UserPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
preferred_language: "English".to_string(),
|
||||
expertise_level: "intermediate".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptServer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
user_preferences: Arc::new(RwLock::new(UserPreferences::default())),
|
||||
prompt_router: Self::prompt_router(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PromptServer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[prompt_router]
|
||||
impl PromptServer {
|
||||
/// Simple greeting prompt without parameters
|
||||
#[prompt(
|
||||
name = "greeting",
|
||||
description = "A simple greeting prompt to start conversations"
|
||||
)]
|
||||
async fn greeting(&self) -> Vec<PromptMessage> {
|
||||
vec![
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
"Hello! I'd like to start our conversation.",
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
"Hello! I'm here to help. What would you like to discuss today?",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Code review prompt with typed parameters
|
||||
#[prompt(
|
||||
name = "code_review",
|
||||
description = "Structured code review with language-specific best practices"
|
||||
)]
|
||||
async fn code_review(
|
||||
&self,
|
||||
Parameters(args): Parameters<CodeReviewArgs>,
|
||||
) -> Result<GetPromptResult, McpError> {
|
||||
let prefs = self.user_preferences.read().await;
|
||||
let focus_areas = args
|
||||
.focus_areas
|
||||
.unwrap_or_else(|| vec!["correctness".to_string(), "performance".to_string()]);
|
||||
|
||||
let messages = vec![
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!(
|
||||
"You are an expert {} code reviewer. The user's expertise level is {}.",
|
||||
args.language, prefs.expertise_level
|
||||
),
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
format!(
|
||||
"Please review the {} code at '{}'. Focus on: {}",
|
||||
args.language,
|
||||
args.file_path,
|
||||
focus_areas.join(", ")
|
||||
),
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!(
|
||||
"I'll review your {} code focusing on {}. Let me analyze the code at '{}'...",
|
||||
args.language,
|
||||
focus_areas.join(" and "),
|
||||
args.file_path
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
Ok(GetPromptResult {
|
||||
description: Some(format!(
|
||||
"Code review for {} file focusing on {}",
|
||||
args.language,
|
||||
focus_areas.join(", ")
|
||||
)),
|
||||
messages,
|
||||
})
|
||||
}
|
||||
|
||||
/// Data analysis prompt demonstrating context usage
|
||||
#[prompt(
|
||||
name = "data_analysis",
|
||||
description = "Analyze data with context-aware suggestions"
|
||||
)]
|
||||
async fn data_analysis(
|
||||
&self,
|
||||
Parameters(args): Parameters<DataAnalysisArgs>,
|
||||
ctx: RequestContext<RoleServer>,
|
||||
) -> Result<Vec<PromptMessage>, McpError> {
|
||||
// Log the request for debugging
|
||||
tracing::info!("Data analysis prompt requested for {} data", args.data_type);
|
||||
|
||||
// Could use ctx to check for capabilities or metadata
|
||||
let _request_id = &ctx.id;
|
||||
|
||||
let context = args
|
||||
.context
|
||||
.unwrap_or_else(|| "General analysis requested".to_string());
|
||||
|
||||
Ok(vec![
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
format!(
|
||||
"I have {} data that needs {} analysis. Context: {}",
|
||||
args.data_type, args.analysis_type, context
|
||||
),
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!(
|
||||
"I'll help you analyze your {} data using {} techniques. Based on your context, \
|
||||
I'll focus on providing actionable insights.",
|
||||
args.data_type, args.analysis_type
|
||||
),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
/// Writing assistant with multiple conversation turns
|
||||
#[prompt(
|
||||
name = "writing_assistant",
|
||||
description = "Multi-turn writing assistance with style guidance"
|
||||
)]
|
||||
async fn writing_assistant(
|
||||
&self,
|
||||
Parameters(args): Parameters<WritingAssistantArgs>,
|
||||
) -> GetPromptResult {
|
||||
let tone = args.tone.unwrap_or_else(|| "professional".to_string());
|
||||
|
||||
let mut messages = vec![
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!(
|
||||
"You are a writing assistant helping create {} content for {}. \
|
||||
Use a {} tone.",
|
||||
args.content_type, args.audience, tone
|
||||
),
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
format!(
|
||||
"I need help writing {} for {}. Key points to cover: {}",
|
||||
args.content_type,
|
||||
args.audience,
|
||||
args.key_points.join(", ")
|
||||
),
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
"I'll help you create that content. Let me structure it based on your key points.",
|
||||
),
|
||||
];
|
||||
|
||||
// Add a message for each key point
|
||||
for (i, point) in args.key_points.iter().enumerate() {
|
||||
messages.push(PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
format!("For point {}: {}, what would you suggest?", i + 1, point),
|
||||
));
|
||||
messages.push(PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!("For '{}', I recommend...", point),
|
||||
));
|
||||
}
|
||||
|
||||
GetPromptResult {
|
||||
description: Some(format!(
|
||||
"Writing {} for {} audience with {} tone",
|
||||
args.content_type, args.audience, tone
|
||||
)),
|
||||
messages,
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug assistant demonstrating error handling patterns
|
||||
#[prompt(
|
||||
name = "debug_assistant",
|
||||
description = "Interactive debugging help with solution tracking"
|
||||
)]
|
||||
async fn debug_assistant(
|
||||
&self,
|
||||
Parameters(args): Parameters<DebugAssistantArgs>,
|
||||
) -> Result<GetPromptResult, McpError> {
|
||||
if args.stack.is_empty() {
|
||||
return Err(McpError::invalid_params(
|
||||
"Technology stack cannot be empty",
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
let mut messages = vec![
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!(
|
||||
"You are a debugging expert for {}. Help diagnose and fix issues.",
|
||||
args.stack.join(", ")
|
||||
),
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
format!(
|
||||
"I'm encountering this error: {}\nStack: {}",
|
||||
args.error_message,
|
||||
args.stack.join(", ")
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// Add tried solutions if any
|
||||
if let Some(tried) = args.tried_solutions {
|
||||
if !tried.is_empty() {
|
||||
messages.push(PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
format!("I've already tried: {}", tried.join(", ")),
|
||||
));
|
||||
messages.push(PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
"I see you've already attempted some solutions. Let me suggest different approaches.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
"Let's debug this systematically. First, let me understand the error context better.",
|
||||
));
|
||||
|
||||
Ok(GetPromptResult {
|
||||
description: Some(format!(
|
||||
"Debugging {} error in {}",
|
||||
args.error_message.chars().take(50).collect::<String>(),
|
||||
args.stack.first().map(|s| s.as_str()).unwrap_or("unknown")
|
||||
)),
|
||||
messages,
|
||||
})
|
||||
}
|
||||
|
||||
/// Learning path prompt that uses server state
|
||||
#[prompt(
|
||||
name = "learning_path",
|
||||
description = "Generate a personalized learning path based on user preferences"
|
||||
)]
|
||||
async fn learning_path(&self) -> Result<Vec<PromptMessage>, McpError> {
|
||||
let prefs = self.user_preferences.read().await;
|
||||
|
||||
Ok(vec![
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!(
|
||||
"Create a learning path for someone at {} level who prefers {} language explanations.",
|
||||
prefs.expertise_level, prefs.preferred_language
|
||||
),
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
"What should I learn next to improve my programming skills?",
|
||||
),
|
||||
PromptMessage::new_text(
|
||||
PromptMessageRole::Assistant,
|
||||
format!(
|
||||
"Based on your {} expertise level, I recommend the following learning path...",
|
||||
prefs.expertise_level
|
||||
),
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[prompt_handler]
|
||||
impl ServerHandler for PromptServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_prompts().build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some(
|
||||
"This server provides various prompt templates for code review, data analysis, \
|
||||
writing assistance, debugging help, and personalized learning paths. \
|
||||
All prompts are designed to provide structured, context-aware assistance."
|
||||
.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
|
||||
.init();
|
||||
|
||||
println!("MCP Prompt Server Example");
|
||||
println!("=======================");
|
||||
println!();
|
||||
println!("This server demonstrates various prompt patterns:");
|
||||
println!("- Simple prompts without parameters");
|
||||
println!("- Prompts with typed parameters");
|
||||
println!("- Prompts using server state");
|
||||
println!("- Multi-turn conversation prompts");
|
||||
println!("- Error handling in prompts");
|
||||
println!();
|
||||
println!("To test with MCP Inspector:");
|
||||
println!(
|
||||
"npx @modelcontextprotocol/inspector cargo run -p mcp-server-examples --example servers_prompt_stdio"
|
||||
);
|
||||
println!();
|
||||
|
||||
let server = PromptServer::new();
|
||||
let service = server.serve(stdio()).await.inspect_err(|e| {
|
||||
tracing::error!("Server error: {:?}", e);
|
||||
})?;
|
||||
|
||||
service.waiting().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
use rmcp::{
|
||||
Json, ServiceExt,
|
||||
handler::server::{router::tool::ToolRouter, tool::Parameters},
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
tool, tool_handler, tool_router,
|
||||
transport::stdio,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue