use crate::context::Context;
use crate::handler::PromptHandler;
use mcpkit_core::error::McpError;
use mcpkit_core::types::prompt::{GetPromptResult, Prompt, PromptArgument, PromptMessage};
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
pub type BoxedPromptFn = Box<
dyn for<'a> Fn(
Option<Value>,
&'a Context<'a>,
)
-> Pin<Box<dyn Future<Output = Result<GetPromptResult, McpError>> + Send + 'a>>
+ Send
+ Sync,
>;
pub struct RegisteredPrompt {
pub prompt: Prompt,
pub handler: BoxedPromptFn,
}
pub struct PromptService {
prompts: HashMap<String, RegisteredPrompt>,
}
impl Default for PromptService {
fn default() -> Self {
Self::new()
}
}
impl PromptService {
#[must_use]
pub fn new() -> Self {
Self {
prompts: HashMap::new(),
}
}
pub fn register<F, Fut>(&mut self, prompt: Prompt, handler: F)
where
F: Fn(Option<Value>, &Context<'_>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<GetPromptResult, McpError>> + Send + 'static,
{
let name = prompt.name.clone();
let boxed: BoxedPromptFn = Box::new(move |args, ctx| Box::pin(handler(args, ctx)));
self.prompts.insert(
name,
RegisteredPrompt {
prompt,
handler: boxed,
},
);
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&RegisteredPrompt> {
self.prompts.get(name)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.prompts.contains_key(name)
}
#[must_use]
pub fn list(&self) -> Vec<&Prompt> {
self.prompts.values().map(|r| &r.prompt).collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.prompts.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.prompts.is_empty()
}
pub async fn render(
&self,
name: &str,
arguments: Option<Value>,
ctx: &Context<'_>,
) -> Result<GetPromptResult, McpError> {
let registered = self.prompts.get(name).ok_or_else(|| {
McpError::invalid_params("prompts/get", format!("Unknown prompt: {name}"))
})?;
(registered.handler)(arguments, ctx).await
}
}
impl PromptHandler for PromptService {
async fn list_prompts(&self, _ctx: &Context<'_>) -> Result<Vec<Prompt>, McpError> {
Ok(self.list().into_iter().cloned().collect())
}
async fn get_prompt(
&self,
name: &str,
arguments: Option<serde_json::Map<String, Value>>,
ctx: &Context<'_>,
) -> Result<GetPromptResult, McpError> {
let args = arguments.map(Value::Object);
self.render(name, args, ctx).await
}
}
pub struct PromptBuilder {
name: String,
description: Option<String>,
arguments: Vec<PromptArgument>,
}
impl PromptBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
arguments: Vec::new(),
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn required_arg(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.arguments.push(PromptArgument {
name: name.into(),
description: Some(description.into()),
required: Some(true),
});
self
}
pub fn optional_arg(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.arguments.push(PromptArgument {
name: name.into(),
description: Some(description.into()),
required: Some(false),
});
self
}
#[must_use]
pub fn argument(mut self, arg: PromptArgument) -> Self {
self.arguments.push(arg);
self
}
#[must_use]
pub fn build(self) -> Prompt {
Prompt {
name: self.name,
description: self.description,
arguments: if self.arguments.is_empty() {
None
} else {
Some(self.arguments)
},
}
}
}
pub struct PromptResultBuilder {
description: Option<String>,
messages: Vec<PromptMessage>,
}
impl Default for PromptResultBuilder {
fn default() -> Self {
Self::new()
}
}
impl PromptResultBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
description: None,
messages: Vec::new(),
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn user_text(mut self, text: impl Into<String>) -> Self {
self.messages.push(PromptMessage::user(text.into()));
self
}
pub fn assistant_text(mut self, text: impl Into<String>) -> Self {
self.messages.push(PromptMessage::assistant(text.into()));
self
}
#[must_use]
pub fn message(mut self, msg: PromptMessage) -> Self {
self.messages.push(msg);
self
}
#[must_use]
pub fn build(self) -> GetPromptResult {
GetPromptResult {
description: self.description,
messages: self.messages,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::{Context, NoOpPeer};
use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
use mcpkit_core::protocol::RequestId;
use mcpkit_core::protocol_version::ProtocolVersion;
fn make_context() -> (
RequestId,
ClientCapabilities,
ServerCapabilities,
ProtocolVersion,
NoOpPeer,
) {
(
RequestId::Number(1),
ClientCapabilities::default(),
ServerCapabilities::default(),
ProtocolVersion::LATEST,
NoOpPeer,
)
}
#[test]
fn test_prompt_builder() {
let prompt = PromptBuilder::new("code-review")
.description("Review code for issues")
.required_arg("code", "The code to review")
.optional_arg("language", "Programming language")
.build();
assert_eq!(prompt.name, "code-review");
assert_eq!(
prompt.description.as_deref(),
Some("Review code for issues")
);
assert_eq!(prompt.arguments.as_ref().map(std::vec::Vec::len), Some(2));
}
#[test]
fn test_prompt_result_builder() {
let result = PromptResultBuilder::new()
.description("Generated review")
.user_text("Please review this code")
.assistant_text("I'll analyze the code...")
.build();
assert_eq!(result.description.as_deref(), Some("Generated review"));
assert_eq!(result.messages.len(), 2);
}
#[tokio::test]
async fn test_prompt_service() -> Result<(), Box<dyn std::error::Error>> {
let mut service = PromptService::new();
let prompt = PromptBuilder::new("greeting")
.description("Generate a greeting")
.required_arg("name", "Name to greet")
.build();
service.register(prompt, |args, _ctx| async move {
let name = args
.and_then(|v| v.get("name").and_then(|n| n.as_str()).map(String::from))
.unwrap_or_else(|| "World".to_string());
Ok(PromptResultBuilder::new()
.user_text(format!("Generate a greeting for {name}"))
.build())
});
assert!(service.contains("greeting"));
assert_eq!(service.len(), 1);
let (req_id, client_caps, server_caps, protocol_version, peer) = make_context();
let ctx = Context::new(
&req_id,
None,
&client_caps,
&server_caps,
protocol_version,
&peer,
);
let result = service
.render("greeting", Some(serde_json::json!({"name": "Alice"})), &ctx)
.await?;
assert!(!result.messages.is_empty());
Ok(())
}
}