Skip to main content

tower_mcp/
prompt.rs

1//! Prompt definition and builder API
2//!
3//! Provides ergonomic ways to define MCP prompts:
4//!
5//! 1. **Builder pattern** - Fluent API for defining prompts
6//! 2. **Trait-based** - Implement `McpPrompt` for full control
7//! 3. **Per-prompt middleware** - Apply tower middleware layers to individual prompts
8//!
9//! # Per-Prompt Middleware
10//!
11//! The `.layer()` method on `PromptBuilder` (after `.handler()`) allows applying
12//! tower middleware to a single prompt. This is useful for prompt-specific concerns
13//! like timeouts, rate limiting, or caching.
14//!
15//! ```rust
16//! use std::collections::HashMap;
17//! use std::time::Duration;
18//! use tower::timeout::TimeoutLayer;
19//! use tower_mcp::prompt::PromptBuilder;
20//! use tower_mcp::protocol::{GetPromptResult, PromptMessage, PromptRole, Content};
21//!
22//! let prompt = PromptBuilder::new("slow_prompt")
23//!     .description("A prompt that might take a while")
24//!     .handler(|args: HashMap<String, String>| async move {
25//!         // Slow prompt generation logic...
26//!         Ok(GetPromptResult {
27//!             description: Some("Generated prompt".to_string()),
28//!             messages: vec![PromptMessage {
29//!                 role: PromptRole::User,
30//!                 content: Content::Text {
31//!                     text: "Hello!".to_string(),
32//!                     annotations: None,
33//!                     meta: None,
34//!                 },
35//!                 meta: None,
36//!             }],
37//!             meta: None,
38//!         })
39//!     })
40//!     .layer(TimeoutLayer::new(Duration::from_secs(5)));
41//!
42//! assert_eq!(prompt.name, "slow_prompt");
43//! ```
44
45use std::collections::HashMap;
46use std::convert::Infallible;
47use std::fmt;
48use std::future::Future;
49use std::pin::Pin;
50use std::sync::Arc;
51use std::task::{Context, Poll};
52
53use pin_project_lite::pin_project;
54
55use tokio::sync::Mutex;
56use tower::util::BoxCloneService;
57use tower::{Layer, ServiceExt};
58use tower_service::Service;
59
60use crate::context::RequestContext;
61use crate::error::{Error, Result};
62use crate::protocol::{
63    Content, GetPromptResult, PromptArgument, PromptDefinition, PromptMessage, PromptRole,
64    RequestId, RequestOutcome, ToolIcon,
65};
66
67/// A boxed future for prompt handlers
68pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
69
70// =============================================================================
71// Per-Prompt Middleware Types
72// =============================================================================
73
74/// Request type for prompt middleware.
75///
76/// Contains the request context and prompt arguments, allowing middleware
77/// to access and modify the request before it reaches the prompt handler.
78#[derive(Debug, Clone)]
79pub struct PromptRequest {
80    /// The request context with progress reporting, cancellation, etc.
81    pub context: RequestContext,
82    /// The prompt arguments (name -> value)
83    pub arguments: HashMap<String, String>,
84}
85
86impl PromptRequest {
87    /// Create a new prompt request with the given context and arguments.
88    pub fn new(context: RequestContext, arguments: HashMap<String, String>) -> Self {
89        Self { context, arguments }
90    }
91
92    /// Create a prompt request with a default context (for testing or simple use cases).
93    pub fn with_arguments(arguments: HashMap<String, String>) -> Self {
94        Self {
95            context: RequestContext::new(RequestId::Number(0)),
96            arguments,
97        }
98    }
99}
100
101/// A boxed, cloneable prompt service with `Error = Infallible`.
102///
103/// This is the service type used internally after applying middleware layers.
104/// It wraps any `Service<PromptRequest>` implementation so that the prompt
105/// handler can consume it without knowing the concrete middleware stack.
106pub type BoxPromptService = BoxCloneService<PromptRequest, GetPromptResult, Infallible>;
107
108#[cfg(feature = "stateless")]
109type BoxMrtrPromptService =
110    BoxCloneService<PromptRequest, RequestOutcome<GetPromptResult>, Infallible>;
111
112/// A service wrapper that catches errors from middleware and converts them
113/// into prompt errors, maintaining the `Error = Infallible` contract.
114///
115/// When a middleware layer (e.g., `TimeoutLayer`) produces an error, this
116/// wrapper converts it into a prompt error. This allows error information to
117/// flow through the normal response path rather than requiring special
118/// error handling.
119#[doc(hidden)]
120pub struct PromptCatchError<S> {
121    inner: S,
122}
123
124impl<S> PromptCatchError<S> {
125    /// Create a new `PromptCatchError` wrapping the given service.
126    pub fn new(inner: S) -> Self {
127        Self { inner }
128    }
129}
130
131impl<S: Clone> Clone for PromptCatchError<S> {
132    fn clone(&self) -> Self {
133        Self {
134            inner: self.inner.clone(),
135        }
136    }
137}
138
139impl<S: fmt::Debug> fmt::Debug for PromptCatchError<S> {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        f.debug_struct("PromptCatchError")
142            .field("inner", &self.inner)
143            .finish()
144    }
145}
146
147pin_project! {
148    /// Future for [`PromptCatchError`].
149    #[doc(hidden)]
150    pub struct PromptCatchErrorFuture<F> {
151        #[pin]
152        inner: F,
153    }
154}
155
156impl<F, E> Future for PromptCatchErrorFuture<F>
157where
158    F: Future<Output = std::result::Result<GetPromptResult, E>>,
159    E: fmt::Display,
160{
161    type Output = std::result::Result<GetPromptResult, Infallible>;
162
163    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
164        match self.project().inner.poll(cx) {
165            Poll::Pending => Poll::Pending,
166            Poll::Ready(Ok(response)) => Poll::Ready(Ok(response)),
167            Poll::Ready(Err(err)) => Poll::Ready(Ok(GetPromptResult {
168                description: Some(format!("Prompt error: {}", err)),
169                messages: vec![PromptMessage {
170                    role: PromptRole::Assistant,
171                    content: Content::Text {
172                        text: format!("Error generating prompt: {}", err),
173                        annotations: None,
174                        meta: None,
175                    },
176                    meta: None,
177                }],
178                meta: None,
179            })),
180        }
181    }
182}
183
184impl<S> Service<PromptRequest> for PromptCatchError<S>
185where
186    S: Service<PromptRequest, Response = GetPromptResult> + Clone + Send + 'static,
187    S::Error: fmt::Display + Send,
188    S::Future: Send,
189{
190    type Response = GetPromptResult;
191    type Error = Infallible;
192    type Future = PromptCatchErrorFuture<S::Future>;
193
194    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
195        self.inner.poll_ready(cx).map_err(|_| unreachable!())
196    }
197
198    fn call(&mut self, req: PromptRequest) -> Self::Future {
199        PromptCatchErrorFuture {
200            inner: self.inner.call(req),
201        }
202    }
203}
204
205#[cfg(feature = "stateless")]
206#[derive(Clone)]
207struct MrtrPromptCatchError<S> {
208    inner: S,
209}
210
211#[cfg(feature = "stateless")]
212impl<S> MrtrPromptCatchError<S> {
213    fn new(inner: S) -> Self {
214        Self { inner }
215    }
216}
217
218#[cfg(feature = "stateless")]
219impl<S> Service<PromptRequest> for MrtrPromptCatchError<S>
220where
221    S: Service<PromptRequest, Response = RequestOutcome<GetPromptResult>> + Clone + Send + 'static,
222    S::Error: fmt::Display + Send + 'static,
223    S::Future: Send + 'static,
224{
225    type Response = RequestOutcome<GetPromptResult>;
226    type Error = Infallible;
227    type Future =
228        Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
229
230    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
231        match self.inner.poll_ready(cx) {
232            Poll::Ready(Ok(())) | Poll::Ready(Err(_)) => Poll::Ready(Ok(())),
233            Poll::Pending => Poll::Pending,
234        }
235    }
236
237    fn call(&mut self, req: PromptRequest) -> Self::Future {
238        let future = self.inner.call(req);
239        Box::pin(async move {
240            Ok(match future.await {
241                Ok(outcome) => outcome,
242                Err(error) => RequestOutcome::Complete(GetPromptResult {
243                    description: Some(format!("Prompt error: {error}")),
244                    messages: vec![PromptMessage {
245                        role: PromptRole::Assistant,
246                        content: Content::Text {
247                            text: format!("Error generating prompt: {error}"),
248                            annotations: None,
249                            meta: None,
250                        },
251                        meta: None,
252                    }],
253                    meta: None,
254                }),
255            })
256        })
257    }
258}
259
260/// Adapts a prompt handler function into a `Service<PromptRequest>`.
261///
262/// This allows the handler to be wrapped with tower middleware layers.
263/// Used by `.layer()` on `PromptBuilderWithHandler`.
264#[doc(hidden)]
265pub struct PromptHandlerService<F> {
266    handler: F,
267}
268
269impl<F> Clone for PromptHandlerService<F>
270where
271    F: Clone,
272{
273    fn clone(&self) -> Self {
274        Self {
275            handler: self.handler.clone(),
276        }
277    }
278}
279
280impl<F, Fut> Service<PromptRequest> for PromptHandlerService<F>
281where
282    F: Fn(HashMap<String, String>) -> Fut + Clone + Send + Sync + 'static,
283    Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
284{
285    type Response = GetPromptResult;
286    type Error = Error;
287    type Future = Pin<Box<dyn Future<Output = std::result::Result<GetPromptResult, Error>> + Send>>;
288
289    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
290        Poll::Ready(Ok(()))
291    }
292
293    fn call(&mut self, req: PromptRequest) -> Self::Future {
294        let handler = self.handler.clone();
295        Box::pin(async move { handler(req.arguments).await })
296    }
297}
298
299/// Adapts a context-aware prompt handler function into a `Service<PromptRequest>`.
300///
301/// Used by `.layer()` on `PromptBuilderWithContextHandler`.
302#[doc(hidden)]
303pub struct PromptContextHandlerService<F> {
304    handler: F,
305}
306
307#[cfg(feature = "stateless")]
308#[doc(hidden)]
309pub struct MrtrPromptHandlerService<F> {
310    handler: F,
311}
312
313#[cfg(feature = "stateless")]
314impl<F: Clone> Clone for MrtrPromptHandlerService<F> {
315    fn clone(&self) -> Self {
316        Self {
317            handler: self.handler.clone(),
318        }
319    }
320}
321
322#[cfg(feature = "stateless")]
323impl<F, Fut> Service<PromptRequest> for MrtrPromptHandlerService<F>
324where
325    F: Fn(RequestContext, HashMap<String, String>) -> Fut + Clone + Send + Sync + 'static,
326    Fut: Future<Output = Result<RequestOutcome<GetPromptResult>>> + Send + 'static,
327{
328    type Response = RequestOutcome<GetPromptResult>;
329    type Error = Error;
330    type Future =
331        Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
332
333    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
334        Poll::Ready(Ok(()))
335    }
336
337    fn call(&mut self, req: PromptRequest) -> Self::Future {
338        let handler = self.handler.clone();
339        Box::pin(async move { handler(req.context, req.arguments).await })
340    }
341}
342
343impl<F> Clone for PromptContextHandlerService<F>
344where
345    F: Clone,
346{
347    fn clone(&self) -> Self {
348        Self {
349            handler: self.handler.clone(),
350        }
351    }
352}
353
354impl<F, Fut> Service<PromptRequest> for PromptContextHandlerService<F>
355where
356    F: Fn(RequestContext, HashMap<String, String>) -> Fut + Clone + Send + Sync + 'static,
357    Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
358{
359    type Response = GetPromptResult;
360    type Error = Error;
361    type Future = Pin<Box<dyn Future<Output = std::result::Result<GetPromptResult, Error>> + Send>>;
362
363    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
364        Poll::Ready(Ok(()))
365    }
366
367    fn call(&mut self, req: PromptRequest) -> Self::Future {
368        let handler = self.handler.clone();
369        Box::pin(async move { handler(req.context, req.arguments).await })
370    }
371}
372
373/// Prompt handler trait - the core abstraction for prompt generation
374pub trait PromptHandler: Send + Sync {
375    /// Get the prompt with the given arguments
376    fn get(&self, arguments: HashMap<String, String>) -> BoxFuture<'_, Result<GetPromptResult>>;
377
378    /// Get the prompt with request context
379    ///
380    /// The default implementation ignores the context and calls `get`.
381    /// Override this to receive context for progress reporting, cancellation, etc.
382    fn get_with_context(
383        &self,
384        _ctx: RequestContext,
385        arguments: HashMap<String, String>,
386    ) -> BoxFuture<'_, Result<GetPromptResult>> {
387        self.get(arguments)
388    }
389
390    /// Returns true if this handler uses context (for optimization)
391    fn uses_context(&self) -> bool {
392        false
393    }
394}
395
396/// Prompt handler that may return an SEP-2322 input-required continuation.
397#[cfg(feature = "stateless")]
398pub trait MrtrPromptHandler: Send + Sync {
399    /// Resolve a prompt attempt with continuation values available through
400    /// the request context.
401    fn get(
402        &self,
403        ctx: RequestContext,
404        arguments: HashMap<String, String>,
405    ) -> BoxFuture<'_, Result<RequestOutcome<GetPromptResult>>>;
406}
407
408/// A complete prompt definition with handler
409pub struct Prompt {
410    /// The prompt name (must be unique within the router).
411    pub name: String,
412    /// Optional human-readable title.
413    pub title: Option<String>,
414    /// Optional description of the prompt.
415    pub description: Option<String>,
416    /// Optional icons for the prompt.
417    pub icons: Option<Vec<ToolIcon>>,
418    /// The arguments this prompt accepts.
419    pub arguments: Vec<PromptArgument>,
420    handler: Option<Arc<dyn PromptHandler>>,
421    #[cfg(feature = "stateless")]
422    mrtr_handler: Option<Arc<dyn MrtrPromptHandler>>,
423}
424
425impl Clone for Prompt {
426    fn clone(&self) -> Self {
427        Self {
428            name: self.name.clone(),
429            title: self.title.clone(),
430            description: self.description.clone(),
431            icons: self.icons.clone(),
432            arguments: self.arguments.clone(),
433            handler: self.handler.clone(),
434            #[cfg(feature = "stateless")]
435            mrtr_handler: self.mrtr_handler.clone(),
436        }
437    }
438}
439
440impl std::fmt::Debug for Prompt {
441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442        f.debug_struct("Prompt")
443            .field("name", &self.name)
444            .field("title", &self.title)
445            .field("description", &self.description)
446            .field("icons", &self.icons)
447            .field("arguments", &self.arguments)
448            .finish_non_exhaustive()
449    }
450}
451
452impl Prompt {
453    /// Create a new prompt builder
454    pub fn builder(name: impl Into<String>) -> PromptBuilder {
455        PromptBuilder::new(name)
456    }
457
458    /// Get the prompt definition for prompts/list
459    pub fn definition(&self) -> PromptDefinition {
460        PromptDefinition {
461            name: self.name.clone(),
462            title: self.title.clone(),
463            description: self.description.clone(),
464            icons: self.icons.clone(),
465            arguments: self.arguments.clone(),
466            meta: None,
467        }
468    }
469
470    /// Get the prompt with arguments
471    pub fn get(
472        &self,
473        arguments: HashMap<String, String>,
474    ) -> BoxFuture<'_, Result<GetPromptResult>> {
475        match &self.handler {
476            Some(handler) => handler.get(arguments),
477            None => Box::pin(async {
478                Err(Error::invalid_params(
479                    "MRTR prompt requires get_outcome_with_context",
480                ))
481            }),
482        }
483    }
484
485    /// Get the prompt with request context
486    ///
487    /// Use this when you have a RequestContext available for progress/cancellation.
488    pub fn get_with_context(
489        &self,
490        ctx: RequestContext,
491        arguments: HashMap<String, String>,
492    ) -> BoxFuture<'_, Result<GetPromptResult>> {
493        match &self.handler {
494            Some(handler) => handler.get_with_context(ctx, arguments),
495            None => Box::pin(async {
496                Err(Error::invalid_params(
497                    "MRTR prompt requires get_outcome_with_context",
498                ))
499            }),
500        }
501    }
502
503    /// Get the prompt while preserving an SEP-2322 input-required outcome.
504    pub fn get_outcome_with_context(
505        &self,
506        ctx: RequestContext,
507        arguments: HashMap<String, String>,
508    ) -> BoxFuture<'_, Result<RequestOutcome<GetPromptResult>>> {
509        #[cfg(feature = "stateless")]
510        if let Some(handler) = &self.mrtr_handler {
511            return handler.get(ctx, arguments);
512        }
513        match &self.handler {
514            Some(handler) => Box::pin(async move {
515                handler
516                    .get_with_context(ctx, arguments)
517                    .await
518                    .map(RequestOutcome::Complete)
519            }),
520            None => Box::pin(async {
521                Err(Error::invalid_params(
522                    "prompt has neither a complete nor MRTR handler",
523                ))
524            }),
525        }
526    }
527
528    /// Returns true if this prompt uses context
529    pub fn uses_context(&self) -> bool {
530        self.handler
531            .as_ref()
532            .is_none_or(|handler| handler.uses_context())
533    }
534}
535
536// =============================================================================
537// Builder API
538// =============================================================================
539
540/// Builder for creating prompts with a fluent API
541///
542/// # Example
543///
544/// ```rust
545/// use tower_mcp::prompt::PromptBuilder;
546/// use tower_mcp::protocol::{GetPromptResult, PromptMessage, PromptRole, Content};
547///
548/// let prompt = PromptBuilder::new("greet")
549///     .description("Generate a greeting")
550///     .required_arg("name", "The name to greet")
551///     .handler(|args| async move {
552///         let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
553///         Ok(GetPromptResult {
554///             description: Some("A greeting prompt".to_string()),
555///             messages: vec![PromptMessage {
556///                 role: PromptRole::User,
557///                 content: Content::Text {
558///                     text: format!("Please greet {}", name),
559///                     annotations: None,
560///                     meta: None,
561///                 },
562///                 meta: None,
563///             }],
564///             meta: None,
565///         })
566///     })
567///     .build();
568///
569/// assert_eq!(prompt.name, "greet");
570/// ```
571pub struct PromptBuilder {
572    name: String,
573    title: Option<String>,
574    description: Option<String>,
575    icons: Option<Vec<ToolIcon>>,
576    arguments: Vec<PromptArgument>,
577}
578
579impl PromptBuilder {
580    /// Create a new prompt builder with the given name.
581    pub fn new(name: impl Into<String>) -> Self {
582        Self {
583            name: name.into(),
584            title: None,
585            description: None,
586            icons: None,
587            arguments: Vec::new(),
588        }
589    }
590
591    /// Set a human-readable title for the prompt
592    pub fn title(mut self, title: impl Into<String>) -> Self {
593        self.title = Some(title.into());
594        self
595    }
596
597    /// Set the prompt description
598    pub fn description(mut self, description: impl Into<String>) -> Self {
599        self.description = Some(description.into());
600        self
601    }
602
603    /// Add an icon for the prompt
604    pub fn icon(mut self, src: impl Into<String>) -> Self {
605        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
606            src: src.into(),
607            mime_type: None,
608            sizes: None,
609            theme: None,
610        });
611        self
612    }
613
614    /// Add an icon with metadata
615    pub fn icon_with_meta(
616        mut self,
617        src: impl Into<String>,
618        mime_type: Option<String>,
619        sizes: Option<Vec<String>>,
620    ) -> Self {
621        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
622            src: src.into(),
623            mime_type,
624            sizes,
625            theme: None,
626        });
627        self
628    }
629
630    /// Add a required argument
631    pub fn required_arg(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
632        self.arguments.push(PromptArgument {
633            name: name.into(),
634            description: Some(description.into()),
635            required: true,
636        });
637        self
638    }
639
640    /// Add an optional argument
641    pub fn optional_arg(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
642        self.arguments.push(PromptArgument {
643            name: name.into(),
644            description: Some(description.into()),
645            required: false,
646        });
647        self
648    }
649
650    /// Add an argument with full control
651    pub fn argument(mut self, arg: PromptArgument) -> Self {
652        self.arguments.push(arg);
653        self
654    }
655
656    /// Set the handler function for getting the prompt.
657    ///
658    /// Returns a `PromptBuilderWithHandler` which can be finalized with `.build()`
659    /// or have middleware applied with `.layer()`.
660    ///
661    /// # Sharing State
662    ///
663    /// Capture an [`Arc`] in the closure to share state across handler
664    /// invocations or with other parts of your application:
665    ///
666    /// ```rust
667    /// use std::collections::HashMap;
668    /// use std::sync::Arc;
669    /// use tokio::sync::RwLock;
670    /// use tower_mcp::prompt::PromptBuilder;
671    /// use tower_mcp::protocol::{GetPromptResult, PromptMessage, PromptRole, Content};
672    ///
673    /// let templates = Arc::new(RwLock::new(HashMap::from([
674    ///     ("greeting".to_string(), "Hello, {name}!".to_string()),
675    /// ])));
676    ///
677    /// let tpl = Arc::clone(&templates);
678    /// let prompt = PromptBuilder::new("greet")
679    ///     .description("Greet a user by name")
680    ///     .required_arg("name", "The user's name")
681    ///     .handler(move |args: HashMap<String, String>| {
682    ///         let tpl = Arc::clone(&tpl);
683    ///         async move {
684    ///             let templates = tpl.read().await;
685    ///             let greeting = templates.get("greeting").unwrap();
686    ///             let name = args.get("name").unwrap();
687    ///             let text = greeting.replace("{name}", name);
688    ///             Ok(GetPromptResult {
689    ///                 description: Some("A greeting".to_string()),
690    ///                 messages: vec![PromptMessage {
691    ///                     role: PromptRole::User,
692    ///                     content: Content::text(text),
693    ///                     meta: None,
694    ///                 }],
695    ///                 meta: None,
696    ///             })
697    ///         }
698    ///     })
699    ///     .build();
700    /// ```
701    ///
702    /// [`Arc`]: std::sync::Arc
703    pub fn handler<F, Fut>(self, handler: F) -> PromptBuilderWithHandler<F>
704    where
705        F: Fn(HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
706        Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
707    {
708        PromptBuilderWithHandler {
709            name: self.name,
710            title: self.title,
711            description: self.description,
712            icons: self.icons,
713            arguments: self.arguments,
714            handler,
715        }
716    }
717
718    /// Set a context-aware handler function for getting the prompt
719    ///
720    /// The handler receives a `RequestContext` for progress reporting and
721    /// cancellation checking, along with the prompt arguments.
722    pub fn handler_with_context<F, Fut>(self, handler: F) -> PromptBuilderWithContextHandler<F>
723    where
724        F: Fn(RequestContext, HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
725        Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
726    {
727        PromptBuilderWithContextHandler {
728            name: self.name,
729            title: self.title,
730            description: self.description,
731            icons: self.icons,
732            arguments: self.arguments,
733            handler,
734        }
735    }
736
737    /// Set an SEP-2322 prompt handler that may return input-required.
738    #[cfg(feature = "stateless")]
739    pub fn mrtr_handler<F, Fut>(self, handler: F) -> PromptBuilderWithMrtrHandler<F>
740    where
741        F: Fn(RequestContext, HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
742        Fut: Future<Output = Result<RequestOutcome<GetPromptResult>>> + Send + 'static,
743    {
744        PromptBuilderWithMrtrHandler {
745            name: self.name,
746            title: self.title,
747            description: self.description,
748            icons: self.icons,
749            arguments: self.arguments,
750            handler,
751        }
752    }
753
754    /// Create a static prompt (no arguments needed)
755    pub fn static_prompt(self, messages: Vec<PromptMessage>) -> Prompt {
756        let description = self.description.clone();
757        self.handler(move |_| {
758            let messages = messages.clone();
759            let description = description.clone();
760            async move {
761                Ok(GetPromptResult {
762                    description,
763                    messages,
764                    meta: None,
765                })
766            }
767        })
768        .build()
769    }
770
771    /// Create a simple text prompt with a user message
772    pub fn user_message(self, text: impl Into<String>) -> Prompt {
773        let text = text.into();
774        self.static_prompt(vec![PromptMessage {
775            role: PromptRole::User,
776            content: Content::Text {
777                text,
778                annotations: None,
779                meta: None,
780            },
781            meta: None,
782        }])
783    }
784
785    /// Finalize the builder into a Prompt
786    ///
787    /// This is an alias for `handler(...).build()` for when you want to
788    /// explicitly mark the build step.
789    pub fn build<F, Fut>(self, handler: F) -> Prompt
790    where
791        F: Fn(HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
792        Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
793    {
794        self.handler(handler).build()
795    }
796}
797
798/// Builder state after handler is specified
799///
800/// This allows either calling `.build()` to create the prompt directly,
801/// or `.layer()` to apply middleware before building.
802#[doc(hidden)]
803pub struct PromptBuilderWithHandler<F> {
804    name: String,
805    title: Option<String>,
806    description: Option<String>,
807    icons: Option<Vec<ToolIcon>>,
808    arguments: Vec<PromptArgument>,
809    handler: F,
810}
811
812#[cfg(feature = "stateless")]
813#[doc(hidden)]
814pub struct PromptBuilderWithMrtrHandler<F> {
815    name: String,
816    title: Option<String>,
817    description: Option<String>,
818    icons: Option<Vec<ToolIcon>>,
819    arguments: Vec<PromptArgument>,
820    handler: F,
821}
822
823impl<F, Fut> PromptBuilderWithHandler<F>
824where
825    F: Fn(HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
826    Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
827{
828    /// Build the prompt without any middleware
829    pub fn build(self) -> Prompt {
830        Prompt {
831            name: self.name,
832            title: self.title,
833            description: self.description,
834            icons: self.icons,
835            arguments: self.arguments,
836            handler: Some(Arc::new(FnHandler {
837                handler: self.handler,
838            })),
839            #[cfg(feature = "stateless")]
840            mrtr_handler: None,
841        }
842    }
843
844    /// Apply a tower middleware layer to this prompt
845    ///
846    /// The layer wraps the prompt handler, allowing middleware like timeouts,
847    /// rate limiting, or retries to be applied to this specific prompt.
848    ///
849    /// # Example
850    ///
851    /// ```rust
852    /// use std::collections::HashMap;
853    /// use std::time::Duration;
854    /// use tower::timeout::TimeoutLayer;
855    /// use tower_mcp::prompt::PromptBuilder;
856    /// use tower_mcp::protocol::{GetPromptResult, PromptMessage, PromptRole, Content};
857    ///
858    /// let prompt = PromptBuilder::new("slow_prompt")
859    ///     .description("A prompt that might take a while")
860    ///     .handler(|_args: HashMap<String, String>| async move {
861    ///         Ok(GetPromptResult {
862    ///             description: Some("Generated prompt".to_string()),
863    ///             messages: vec![PromptMessage {
864    ///                 role: PromptRole::User,
865    ///                 content: Content::Text {
866    ///                     text: "Hello!".to_string(),
867    ///                     annotations: None,
868    ///                     meta: None,
869    ///                 },
870    ///                 meta: None,
871    ///             }],
872    ///             meta: None,
873    ///         })
874    ///     })
875    ///     .layer(TimeoutLayer::new(Duration::from_secs(5)));
876    /// ```
877    pub fn layer<L>(self, layer: L) -> Prompt
878    where
879        L: Layer<PromptHandlerService<F>> + Send + Sync + 'static,
880        L::Service: Service<PromptRequest, Response = GetPromptResult> + Clone + Send + 'static,
881        <L::Service as Service<PromptRequest>>::Error: fmt::Display + Send,
882        <L::Service as Service<PromptRequest>>::Future: Send,
883    {
884        let service = PromptHandlerService {
885            handler: self.handler,
886        };
887        let wrapped = layer.layer(service);
888        let boxed = BoxCloneService::new(PromptCatchError::new(wrapped));
889
890        Prompt {
891            name: self.name,
892            title: self.title,
893            description: self.description,
894            icons: self.icons,
895            arguments: self.arguments,
896            handler: Some(Arc::new(ServiceHandler {
897                service: Mutex::new(boxed),
898            })),
899            #[cfg(feature = "stateless")]
900            mrtr_handler: None,
901        }
902    }
903}
904
905#[cfg(feature = "stateless")]
906impl<F, Fut> PromptBuilderWithMrtrHandler<F>
907where
908    F: Fn(RequestContext, HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
909    Fut: Future<Output = Result<RequestOutcome<GetPromptResult>>> + Send + 'static,
910{
911    /// Build the MRTR-capable prompt.
912    pub fn build(self) -> Prompt {
913        Prompt {
914            name: self.name,
915            title: self.title,
916            description: self.description,
917            icons: self.icons,
918            arguments: self.arguments,
919            handler: None,
920            mrtr_handler: Some(Arc::new(MrtrContextHandler {
921                handler: self.handler,
922            })),
923        }
924    }
925
926    /// Apply a Tower layer to every attempt at this MRTR-capable prompt.
927    ///
928    /// Each retry is an independent request, so the layer runs once per
929    /// round. Middleware failures become complete prompt error results,
930    /// matching non-MRTR prompt middleware.
931    #[allow(private_bounds)]
932    pub fn layer<L>(self, layer: L) -> Prompt
933    where
934        L: Layer<MrtrPromptHandlerService<F>> + Send + Sync + 'static,
935        L::Service: Service<PromptRequest, Response = RequestOutcome<GetPromptResult>>
936            + Clone
937            + Send
938            + 'static,
939        <L::Service as Service<PromptRequest>>::Error: fmt::Display + Send + 'static,
940        <L::Service as Service<PromptRequest>>::Future: Send + 'static,
941    {
942        let service = MrtrPromptHandlerService {
943            handler: self.handler,
944        };
945        let service = layer.layer(service);
946        let service = BoxCloneService::new(MrtrPromptCatchError::new(service));
947
948        Prompt {
949            name: self.name,
950            title: self.title,
951            description: self.description,
952            icons: self.icons,
953            arguments: self.arguments,
954            handler: None,
955            mrtr_handler: Some(Arc::new(ServiceMrtrPromptHandler {
956                service: Mutex::new(service),
957            })),
958        }
959    }
960}
961
962/// Builder state after context-aware handler is specified
963#[doc(hidden)]
964pub struct PromptBuilderWithContextHandler<F> {
965    name: String,
966    title: Option<String>,
967    description: Option<String>,
968    icons: Option<Vec<ToolIcon>>,
969    arguments: Vec<PromptArgument>,
970    handler: F,
971}
972
973impl<F, Fut> PromptBuilderWithContextHandler<F>
974where
975    F: Fn(RequestContext, HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
976    Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
977{
978    /// Build the prompt without any middleware
979    pub fn build(self) -> Prompt {
980        Prompt {
981            name: self.name,
982            title: self.title,
983            description: self.description,
984            icons: self.icons,
985            arguments: self.arguments,
986            handler: Some(Arc::new(ContextAwareHandler {
987                handler: self.handler,
988            })),
989            #[cfg(feature = "stateless")]
990            mrtr_handler: None,
991        }
992    }
993
994    /// Apply a tower middleware layer to this prompt
995    pub fn layer<L>(self, layer: L) -> Prompt
996    where
997        L: Layer<PromptContextHandlerService<F>> + Send + Sync + 'static,
998        L::Service: Service<PromptRequest, Response = GetPromptResult> + Clone + Send + 'static,
999        <L::Service as Service<PromptRequest>>::Error: fmt::Display + Send,
1000        <L::Service as Service<PromptRequest>>::Future: Send,
1001    {
1002        let service = PromptContextHandlerService {
1003            handler: self.handler,
1004        };
1005        let wrapped = layer.layer(service);
1006        let boxed = BoxCloneService::new(PromptCatchError::new(wrapped));
1007
1008        Prompt {
1009            name: self.name,
1010            title: self.title,
1011            description: self.description,
1012            icons: self.icons,
1013            arguments: self.arguments,
1014            handler: Some(Arc::new(ServiceContextHandler {
1015                service: Mutex::new(boxed),
1016            })),
1017            #[cfg(feature = "stateless")]
1018            mrtr_handler: None,
1019        }
1020    }
1021}
1022
1023// =============================================================================
1024// Handler implementations
1025// =============================================================================
1026
1027/// Handler wrapping a function
1028struct FnHandler<F> {
1029    handler: F,
1030}
1031
1032impl<F, Fut> PromptHandler for FnHandler<F>
1033where
1034    F: Fn(HashMap<String, String>) -> Fut + Send + Sync + 'static,
1035    Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
1036{
1037    fn get(&self, arguments: HashMap<String, String>) -> BoxFuture<'_, Result<GetPromptResult>> {
1038        Box::pin((self.handler)(arguments))
1039    }
1040}
1041
1042/// Handler that receives request context
1043struct ContextAwareHandler<F> {
1044    handler: F,
1045}
1046
1047#[cfg(feature = "stateless")]
1048struct MrtrContextHandler<F> {
1049    handler: F,
1050}
1051
1052#[cfg(feature = "stateless")]
1053struct ServiceMrtrPromptHandler {
1054    service: Mutex<BoxMrtrPromptService>,
1055}
1056
1057#[cfg(feature = "stateless")]
1058impl MrtrPromptHandler for ServiceMrtrPromptHandler {
1059    fn get(
1060        &self,
1061        ctx: RequestContext,
1062        arguments: HashMap<String, String>,
1063    ) -> BoxFuture<'_, Result<RequestOutcome<GetPromptResult>>> {
1064        Box::pin(async move {
1065            let request = PromptRequest::new(ctx, arguments);
1066            let mut service = self.service.lock().await.clone();
1067            let outcome = service
1068                .ready()
1069                .await
1070                .expect("MRTR prompt service is infallible")
1071                .call(request)
1072                .await
1073                .expect("MRTR prompt service is infallible");
1074            Ok(outcome)
1075        })
1076    }
1077}
1078
1079#[cfg(feature = "stateless")]
1080impl<F, Fut> MrtrPromptHandler for MrtrContextHandler<F>
1081where
1082    F: Fn(RequestContext, HashMap<String, String>) -> Fut + Send + Sync + 'static,
1083    Fut: Future<Output = Result<RequestOutcome<GetPromptResult>>> + Send + 'static,
1084{
1085    fn get(
1086        &self,
1087        ctx: RequestContext,
1088        arguments: HashMap<String, String>,
1089    ) -> BoxFuture<'_, Result<RequestOutcome<GetPromptResult>>> {
1090        Box::pin((self.handler)(ctx, arguments))
1091    }
1092}
1093
1094impl<F, Fut> PromptHandler for ContextAwareHandler<F>
1095where
1096    F: Fn(RequestContext, HashMap<String, String>) -> Fut + Send + Sync + 'static,
1097    Fut: Future<Output = Result<GetPromptResult>> + Send + 'static,
1098{
1099    fn get(&self, arguments: HashMap<String, String>) -> BoxFuture<'_, Result<GetPromptResult>> {
1100        // When called without context, create a dummy context
1101        let ctx = RequestContext::new(RequestId::Number(0));
1102        self.get_with_context(ctx, arguments)
1103    }
1104
1105    fn get_with_context(
1106        &self,
1107        ctx: RequestContext,
1108        arguments: HashMap<String, String>,
1109    ) -> BoxFuture<'_, Result<GetPromptResult>> {
1110        Box::pin((self.handler)(ctx, arguments))
1111    }
1112
1113    fn uses_context(&self) -> bool {
1114        true
1115    }
1116}
1117
1118/// Handler wrapping a boxed service (used when middleware is applied)
1119///
1120/// Uses a Mutex to make the BoxCloneService (which is Send but not Sync) safe
1121/// for use in a Sync context. Since we clone the service before each call,
1122/// the lock is only held briefly during the clone.
1123struct ServiceHandler {
1124    service: Mutex<BoxPromptService>,
1125}
1126
1127impl PromptHandler for ServiceHandler {
1128    fn get(&self, arguments: HashMap<String, String>) -> BoxFuture<'_, Result<GetPromptResult>> {
1129        Box::pin(async move {
1130            let req = PromptRequest::with_arguments(arguments);
1131            let mut service = self.service.lock().await.clone();
1132            match service.ready().await {
1133                Ok(svc) => svc.call(req).await.map_err(|e| match e {}),
1134                Err(e) => match e {},
1135            }
1136        })
1137    }
1138
1139    fn get_with_context(
1140        &self,
1141        ctx: RequestContext,
1142        arguments: HashMap<String, String>,
1143    ) -> BoxFuture<'_, Result<GetPromptResult>> {
1144        Box::pin(async move {
1145            let req = PromptRequest::new(ctx, arguments);
1146            let mut service = self.service.lock().await.clone();
1147            match service.ready().await {
1148                Ok(svc) => svc.call(req).await.map_err(|e| match e {}),
1149                Err(e) => match e {},
1150            }
1151        })
1152    }
1153}
1154
1155/// Handler wrapping a boxed service for context-aware prompts
1156struct ServiceContextHandler {
1157    service: Mutex<BoxPromptService>,
1158}
1159
1160impl PromptHandler for ServiceContextHandler {
1161    fn get(&self, arguments: HashMap<String, String>) -> BoxFuture<'_, Result<GetPromptResult>> {
1162        let ctx = RequestContext::new(RequestId::Number(0));
1163        self.get_with_context(ctx, arguments)
1164    }
1165
1166    fn get_with_context(
1167        &self,
1168        ctx: RequestContext,
1169        arguments: HashMap<String, String>,
1170    ) -> BoxFuture<'_, Result<GetPromptResult>> {
1171        Box::pin(async move {
1172            let req = PromptRequest::new(ctx, arguments);
1173            let mut service = self.service.lock().await.clone();
1174            match service.ready().await {
1175                Ok(svc) => svc.call(req).await.map_err(|e| match e {}),
1176                Err(e) => match e {},
1177            }
1178        })
1179    }
1180
1181    fn uses_context(&self) -> bool {
1182        true
1183    }
1184}
1185
1186// =============================================================================
1187// Trait-based prompt definition
1188// =============================================================================
1189
1190/// Trait for defining prompts with full control
1191///
1192/// Implement this trait when you need more control than the builder provides,
1193/// or when you want to define prompts as standalone types.
1194///
1195/// # Example
1196///
1197/// ```rust
1198/// use std::collections::HashMap;
1199/// use tower_mcp::prompt::McpPrompt;
1200/// use tower_mcp::protocol::{GetPromptResult, PromptArgument, PromptMessage, PromptRole, Content};
1201/// use tower_mcp::error::Result;
1202///
1203/// struct CodeReviewPrompt;
1204///
1205/// impl McpPrompt for CodeReviewPrompt {
1206///     const NAME: &'static str = "code_review";
1207///     const DESCRIPTION: &'static str = "Review code for issues";
1208///
1209///     fn arguments(&self) -> Vec<PromptArgument> {
1210///         vec![
1211///             PromptArgument {
1212///                 name: "code".to_string(),
1213///                 description: Some("The code to review".to_string()),
1214///                 required: true,
1215///             },
1216///             PromptArgument {
1217///                 name: "language".to_string(),
1218///                 description: Some("Programming language".to_string()),
1219///                 required: false,
1220///             },
1221///         ]
1222///     }
1223///
1224///     async fn get(&self, args: HashMap<String, String>) -> Result<GetPromptResult> {
1225///         let code = args.get("code").map(|s| s.as_str()).unwrap_or("");
1226///         let lang = args.get("language").map(|s| s.as_str()).unwrap_or("unknown");
1227///
1228///         Ok(GetPromptResult {
1229///             description: Some("Code review prompt".to_string()),
1230///             messages: vec![PromptMessage {
1231///                 role: PromptRole::User,
1232///                 content: Content::Text {
1233///                     text: format!("Please review this {} code:\n\n```{}\n{}\n```", lang, lang, code),
1234///                     annotations: None,
1235///                     meta: None,
1236///                 },
1237///                 meta: None,
1238///             }],
1239///             meta: None,
1240///         })
1241///     }
1242/// }
1243///
1244/// let prompt = CodeReviewPrompt.into_prompt();
1245/// assert_eq!(prompt.name, "code_review");
1246/// ```
1247pub trait McpPrompt: Send + Sync + 'static {
1248    /// The prompt name (must be unique within the router).
1249    const NAME: &'static str;
1250    /// A human-readable description of the prompt.
1251    const DESCRIPTION: &'static str;
1252
1253    /// Define the arguments for this prompt
1254    fn arguments(&self) -> Vec<PromptArgument> {
1255        Vec::new()
1256    }
1257
1258    /// Generate the prompt messages for the given arguments.
1259    fn get(
1260        &self,
1261        arguments: HashMap<String, String>,
1262    ) -> impl Future<Output = Result<GetPromptResult>> + Send;
1263
1264    /// Convert to a Prompt instance
1265    fn into_prompt(self) -> Prompt
1266    where
1267        Self: Sized,
1268    {
1269        let arguments = self.arguments();
1270        let prompt = Arc::new(self);
1271        Prompt {
1272            name: Self::NAME.to_string(),
1273            title: None,
1274            description: Some(Self::DESCRIPTION.to_string()),
1275            icons: None,
1276            arguments,
1277            handler: Some(Arc::new(McpPromptHandler { prompt })),
1278            #[cfg(feature = "stateless")]
1279            mrtr_handler: None,
1280        }
1281    }
1282}
1283
1284/// Wrapper to make McpPrompt implement PromptHandler
1285struct McpPromptHandler<T: McpPrompt> {
1286    prompt: Arc<T>,
1287}
1288
1289impl<T: McpPrompt> PromptHandler for McpPromptHandler<T> {
1290    fn get(&self, arguments: HashMap<String, String>) -> BoxFuture<'_, Result<GetPromptResult>> {
1291        let prompt = self.prompt.clone();
1292        Box::pin(async move { prompt.get(arguments).await })
1293    }
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298    use super::*;
1299
1300    #[tokio::test]
1301    async fn test_builder_prompt() {
1302        let prompt = PromptBuilder::new("greet")
1303            .description("A greeting prompt")
1304            .required_arg("name", "Name to greet")
1305            .handler(|args| async move {
1306                let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
1307                Ok(GetPromptResult {
1308                    description: Some("Greeting".to_string()),
1309                    messages: vec![PromptMessage {
1310                        role: PromptRole::User,
1311                        content: Content::Text {
1312                            text: format!("Hello, {}!", name),
1313                            annotations: None,
1314                            meta: None,
1315                        },
1316                        meta: None,
1317                    }],
1318                    meta: None,
1319                })
1320            })
1321            .build();
1322
1323        assert_eq!(prompt.name, "greet");
1324        assert_eq!(prompt.description.as_deref(), Some("A greeting prompt"));
1325        assert_eq!(prompt.arguments.len(), 1);
1326        assert!(prompt.arguments[0].required);
1327
1328        let mut args = HashMap::new();
1329        args.insert("name".to_string(), "Alice".to_string());
1330        let result = prompt.get(args).await.unwrap();
1331
1332        assert_eq!(result.messages.len(), 1);
1333        match &result.messages[0].content {
1334            Content::Text { text, .. } => assert_eq!(text, "Hello, Alice!"),
1335            _ => panic!("Expected text content"),
1336        }
1337    }
1338
1339    #[tokio::test]
1340    async fn test_static_prompt() {
1341        let prompt = PromptBuilder::new("help")
1342            .description("Help prompt")
1343            .user_message("How can I help you today?");
1344
1345        let result = prompt.get(HashMap::new()).await.unwrap();
1346        assert_eq!(result.messages.len(), 1);
1347        match &result.messages[0].content {
1348            Content::Text { text, .. } => assert_eq!(text, "How can I help you today?"),
1349            _ => panic!("Expected text content"),
1350        }
1351    }
1352
1353    #[tokio::test]
1354    async fn test_trait_prompt() {
1355        struct TestPrompt;
1356
1357        impl McpPrompt for TestPrompt {
1358            const NAME: &'static str = "test";
1359            const DESCRIPTION: &'static str = "A test prompt";
1360
1361            fn arguments(&self) -> Vec<PromptArgument> {
1362                vec![PromptArgument {
1363                    name: "input".to_string(),
1364                    description: Some("Test input".to_string()),
1365                    required: true,
1366                }]
1367            }
1368
1369            async fn get(&self, args: HashMap<String, String>) -> Result<GetPromptResult> {
1370                let input = args.get("input").map(|s| s.as_str()).unwrap_or("default");
1371                Ok(GetPromptResult {
1372                    description: Some("Test".to_string()),
1373                    messages: vec![PromptMessage {
1374                        role: PromptRole::User,
1375                        content: Content::Text {
1376                            text: format!("Input: {}", input),
1377                            annotations: None,
1378                            meta: None,
1379                        },
1380                        meta: None,
1381                    }],
1382                    meta: None,
1383                })
1384            }
1385        }
1386
1387        let prompt = TestPrompt.into_prompt();
1388        assert_eq!(prompt.name, "test");
1389        assert_eq!(prompt.arguments.len(), 1);
1390
1391        let mut args = HashMap::new();
1392        args.insert("input".to_string(), "hello".to_string());
1393        let result = prompt.get(args).await.unwrap();
1394
1395        match &result.messages[0].content {
1396            Content::Text { text, .. } => assert_eq!(text, "Input: hello"),
1397            _ => panic!("Expected text content"),
1398        }
1399    }
1400
1401    #[test]
1402    fn test_prompt_definition() {
1403        let prompt = PromptBuilder::new("test")
1404            .description("Test description")
1405            .required_arg("arg1", "First arg")
1406            .optional_arg("arg2", "Second arg")
1407            .user_message("Test");
1408
1409        let def = prompt.definition();
1410        assert_eq!(def.name, "test");
1411        assert_eq!(def.description.as_deref(), Some("Test description"));
1412        assert_eq!(def.arguments.len(), 2);
1413        assert!(def.arguments[0].required);
1414        assert!(!def.arguments[1].required);
1415    }
1416
1417    #[tokio::test]
1418    async fn test_handler_with_context() {
1419        let prompt = PromptBuilder::new("context_prompt")
1420            .description("A prompt with context")
1421            .handler_with_context(|ctx: RequestContext, args| async move {
1422                // Verify we have access to the context
1423                let _ = ctx.is_cancelled();
1424                let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
1425                Ok(GetPromptResult {
1426                    description: Some("Context prompt".to_string()),
1427                    messages: vec![PromptMessage {
1428                        role: PromptRole::User,
1429                        content: Content::Text {
1430                            text: format!("Hello, {}!", name),
1431                            annotations: None,
1432                            meta: None,
1433                        },
1434                        meta: None,
1435                    }],
1436                    meta: None,
1437                })
1438            })
1439            .build();
1440
1441        assert_eq!(prompt.name, "context_prompt");
1442        assert!(prompt.uses_context());
1443
1444        let ctx = RequestContext::new(RequestId::Number(1));
1445        let mut args = HashMap::new();
1446        args.insert("name".to_string(), "Alice".to_string());
1447        let result = prompt.get_with_context(ctx, args).await.unwrap();
1448
1449        match &result.messages[0].content {
1450            Content::Text { text, .. } => assert_eq!(text, "Hello, Alice!"),
1451            _ => panic!("Expected text content"),
1452        }
1453    }
1454
1455    #[tokio::test]
1456    async fn test_prompt_with_timeout_layer() {
1457        use std::time::Duration;
1458        use tower::timeout::TimeoutLayer;
1459
1460        let prompt = PromptBuilder::new("timeout_prompt")
1461            .description("A prompt with timeout")
1462            .handler(|args: HashMap<String, String>| async move {
1463                let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
1464                Ok(GetPromptResult {
1465                    description: Some("Timeout prompt".to_string()),
1466                    messages: vec![PromptMessage {
1467                        role: PromptRole::User,
1468                        content: Content::Text {
1469                            text: format!("Hello, {}!", name),
1470                            annotations: None,
1471                            meta: None,
1472                        },
1473                        meta: None,
1474                    }],
1475                    meta: None,
1476                })
1477            })
1478            .layer(TimeoutLayer::new(Duration::from_secs(5)));
1479
1480        assert_eq!(prompt.name, "timeout_prompt");
1481
1482        let mut args = HashMap::new();
1483        args.insert("name".to_string(), "Alice".to_string());
1484        let result = prompt.get(args).await.unwrap();
1485
1486        match &result.messages[0].content {
1487            Content::Text { text, .. } => assert_eq!(text, "Hello, Alice!"),
1488            _ => panic!("Expected text content"),
1489        }
1490    }
1491
1492    #[tokio::test]
1493    async fn test_prompt_timeout_expires() {
1494        use std::time::Duration;
1495        use tower::timeout::TimeoutLayer;
1496
1497        let prompt = PromptBuilder::new("slow_prompt")
1498            .description("A slow prompt")
1499            .handler(|_args: HashMap<String, String>| async move {
1500                // Sleep much longer than timeout to ensure timeout fires reliably in CI
1501                tokio::time::sleep(Duration::from_secs(1)).await;
1502                Ok(GetPromptResult {
1503                    description: Some("Slow prompt".to_string()),
1504                    messages: vec![PromptMessage {
1505                        role: PromptRole::User,
1506                        content: Content::Text {
1507                            text: "This should not appear".to_string(),
1508                            annotations: None,
1509                            meta: None,
1510                        },
1511                        meta: None,
1512                    }],
1513                    meta: None,
1514                })
1515            })
1516            .layer(TimeoutLayer::new(Duration::from_millis(50)));
1517
1518        let result = prompt.get(HashMap::new()).await.unwrap();
1519
1520        // Should get an error message due to timeout
1521        assert!(result.description.as_ref().unwrap().contains("error"));
1522        match &result.messages[0].content {
1523            Content::Text { text, .. } => {
1524                assert!(text.contains("Error generating prompt"));
1525            }
1526            _ => panic!("Expected text content"),
1527        }
1528    }
1529
1530    #[tokio::test]
1531    async fn test_context_handler_with_layer() {
1532        use std::time::Duration;
1533        use tower::timeout::TimeoutLayer;
1534
1535        let prompt = PromptBuilder::new("context_timeout")
1536            .description("Context prompt with timeout")
1537            .handler_with_context(
1538                |_ctx: RequestContext, args: HashMap<String, String>| async move {
1539                    let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
1540                    Ok(GetPromptResult {
1541                        description: Some("Context timeout".to_string()),
1542                        messages: vec![PromptMessage {
1543                            role: PromptRole::User,
1544                            content: Content::Text {
1545                                text: format!("Hello, {}!", name),
1546                                annotations: None,
1547                                meta: None,
1548                            },
1549                            meta: None,
1550                        }],
1551                        meta: None,
1552                    })
1553                },
1554            )
1555            .layer(TimeoutLayer::new(Duration::from_secs(5)));
1556
1557        assert_eq!(prompt.name, "context_timeout");
1558        assert!(prompt.uses_context());
1559
1560        let ctx = RequestContext::new(RequestId::Number(1));
1561        let mut args = HashMap::new();
1562        args.insert("name".to_string(), "Bob".to_string());
1563        let result = prompt.get_with_context(ctx, args).await.unwrap();
1564
1565        match &result.messages[0].content {
1566            Content::Text { text, .. } => assert_eq!(text, "Hello, Bob!"),
1567            _ => panic!("Expected text content"),
1568        }
1569    }
1570
1571    #[test]
1572    fn test_prompt_request_construction() {
1573        let args: HashMap<String, String> = [("key".to_string(), "value".to_string())]
1574            .into_iter()
1575            .collect();
1576
1577        let req = PromptRequest::with_arguments(args.clone());
1578        assert_eq!(req.arguments.get("key"), Some(&"value".to_string()));
1579
1580        let ctx = RequestContext::new(RequestId::Number(42));
1581        let req2 = PromptRequest::new(ctx, args);
1582        assert_eq!(req2.arguments.get("key"), Some(&"value".to_string()));
1583    }
1584
1585    #[test]
1586    fn test_prompt_catch_error_clone() {
1587        // Just verify the type can be constructed and cloned
1588        let handler = PromptHandlerService {
1589            handler: |_args: HashMap<String, String>| async {
1590                Ok::<GetPromptResult, Error>(GetPromptResult {
1591                    description: None,
1592                    messages: vec![],
1593                    meta: None,
1594                })
1595            },
1596        };
1597        let catch_error = PromptCatchError::new(handler);
1598        let _clone = catch_error.clone();
1599        // PromptCatchError with PromptHandlerService doesn't implement Debug
1600        // because the handler function doesn't implement Debug
1601    }
1602
1603    #[tokio::test]
1604    async fn test_prompt_handler_with_arguments() {
1605        let prompt = PromptBuilder::new("greet")
1606            .description("Greeting prompt")
1607            .required_arg("name", "Person to greet")
1608            .optional_arg("style", "Greeting style")
1609            .handler(|args: HashMap<String, String>| async move {
1610                let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
1611                let style = args.get("style").map(|s| s.as_str()).unwrap_or("casual");
1612                let text = match style {
1613                    "formal" => format!("Good day, {name}."),
1614                    _ => format!("Hey {name}!"),
1615                };
1616                Ok(GetPromptResult::user_message(text))
1617            })
1618            .build();
1619
1620        // Test with both arguments
1621        let mut args = HashMap::new();
1622        args.insert("name".to_string(), "Alice".to_string());
1623        args.insert("style".to_string(), "formal".to_string());
1624        let result = prompt.get(args).await.unwrap();
1625        assert_eq!(result.messages.len(), 1);
1626
1627        // Test with required arg only
1628        let mut args = HashMap::new();
1629        args.insert("name".to_string(), "Bob".to_string());
1630        let result = prompt.get(args).await.unwrap();
1631        assert_eq!(result.messages.len(), 1);
1632    }
1633
1634    #[cfg(feature = "stateless")]
1635    #[tokio::test]
1636    async fn test_mrtr_builder_preserves_input_required_outcome() {
1637        let prompt = PromptBuilder::new("continue")
1638            .mrtr_handler(|_ctx, _args| async move {
1639                Ok(RequestOutcome::input_required(
1640                    crate::protocol::InputRequiredResult::new().with_request_state("signed-state"),
1641                ))
1642            })
1643            .build();
1644
1645        let outcome = prompt
1646            .get_outcome_with_context(RequestContext::new(RequestId::Number(1)), HashMap::new())
1647            .await
1648            .unwrap();
1649        assert_eq!(
1650            outcome
1651                .as_input_required()
1652                .and_then(|result| result.request_state.as_deref()),
1653            Some("signed-state")
1654        );
1655    }
1656
1657    #[cfg(feature = "stateless")]
1658    #[tokio::test]
1659    async fn mrtr_prompt_composes_middleware() {
1660        use std::time::Duration;
1661        use tower::timeout::TimeoutLayer;
1662
1663        let prompt = PromptBuilder::new("layered_continue")
1664            .mrtr_handler(|_ctx, _args| async move {
1665                Ok(RequestOutcome::input_required(
1666                    crate::protocol::InputRequiredResult::new().with_request_state("layered-state"),
1667                ))
1668            })
1669            .layer(TimeoutLayer::new(Duration::from_secs(1)));
1670
1671        let outcome = prompt
1672            .get_outcome_with_context(RequestContext::new(RequestId::Number(2)), HashMap::new())
1673            .await
1674            .unwrap();
1675        assert_eq!(
1676            outcome
1677                .as_input_required()
1678                .and_then(|result| result.request_state.as_deref()),
1679            Some("layered-state")
1680        );
1681    }
1682
1683    #[tokio::test]
1684    async fn test_prompt_definition_fields() {
1685        let prompt = PromptBuilder::new("test_prompt")
1686            .title("Test Prompt")
1687            .description("A test prompt")
1688            .required_arg("input", "The input")
1689            .optional_arg("format", "Output format")
1690            .handler(|_args: HashMap<String, String>| async move {
1691                Ok(GetPromptResult::user_message("test"))
1692            })
1693            .build();
1694
1695        let def = prompt.definition();
1696        assert_eq!(def.name, "test_prompt");
1697        assert_eq!(def.title.as_deref(), Some("Test Prompt"));
1698        assert_eq!(def.description.as_deref(), Some("A test prompt"));
1699        assert_eq!(def.arguments.len(), 2);
1700        assert!(def.arguments[0].required);
1701        assert!(!def.arguments[1].required);
1702    }
1703
1704    #[tokio::test]
1705    async fn test_prompt_with_context_handler() {
1706        let prompt = PromptBuilder::new("ctx_prompt")
1707            .description("Context-aware prompt")
1708            .handler_with_context(
1709                |ctx: RequestContext, args: HashMap<String, String>| async move {
1710                    let _ = ctx;
1711                    let name = args.get("name").map(|s| s.as_str()).unwrap_or("default");
1712                    Ok(GetPromptResult::user_message(format!("ctx: {name}")))
1713                },
1714            )
1715            .build();
1716
1717        assert!(prompt.uses_context());
1718
1719        let mut args = HashMap::new();
1720        args.insert("name".to_string(), "test".to_string());
1721        let ctx = RequestContext::new(RequestId::Number(1));
1722        let result: std::result::Result<GetPromptResult, Error> =
1723            prompt.get_with_context(ctx, args).await;
1724        assert!(result.is_ok());
1725        assert_eq!(result.unwrap().messages.len(), 1);
1726    }
1727
1728    #[tokio::test]
1729    async fn test_prompt_with_layer_catches_timeout() {
1730        use std::time::Duration;
1731        use tower::timeout::TimeoutLayer;
1732
1733        let prompt = PromptBuilder::new("slow_prompt")
1734            .description("Will timeout")
1735            .handler(|_args: HashMap<String, String>| async move {
1736                tokio::time::sleep(Duration::from_secs(10)).await;
1737                Ok(GetPromptResult::user_message("too late"))
1738            })
1739            .layer(TimeoutLayer::new(Duration::from_millis(10)));
1740
1741        // The prompt goes through ServiceHandler -> PromptCatchError which
1742        // converts the timeout error into a GetPromptResult with an error message.
1743        // The .get() method delegates through the handler trait.
1744        let result = prompt.get(HashMap::new()).await;
1745        // PromptCatchError converts middleware errors to Ok(GetPromptResult)
1746        // with the error message in the prompt content.
1747        match result {
1748            Ok(r) => {
1749                // Should contain timeout error text in the message
1750                assert!(
1751                    !r.messages.is_empty(),
1752                    "Expected error message in prompt result"
1753                );
1754            }
1755            Err(_) => {
1756                // Also acceptable -- error propagated directly
1757            }
1758        }
1759    }
1760
1761    #[tokio::test]
1762    async fn test_prompt_clone() {
1763        let prompt = PromptBuilder::new("cloneable")
1764            .description("Can be cloned")
1765            .handler(|_args: HashMap<String, String>| async move {
1766                Ok(GetPromptResult::user_message("original"))
1767            })
1768            .build();
1769
1770        let cloned = prompt.clone();
1771        assert_eq!(cloned.name, "cloneable");
1772
1773        let result = cloned.get(HashMap::new()).await.unwrap();
1774        assert_eq!(result.messages.len(), 1);
1775    }
1776}