Skip to main content

rmcp/handler/server/
prompt.rs

1//! Prompt handling infrastructure for MCP servers
2//!
3//! This module provides the core types and traits for implementing prompt handlers
4//! in MCP servers. Prompts allow servers to provide reusable templates for LLM
5//! interactions with customizable arguments.
6
7use std::{future::Future, marker::PhantomData};
8
9#[cfg(not(feature = "local"))]
10use futures::future::BoxFuture;
11use serde::de::DeserializeOwned;
12
13use super::common::{AsRequestContext, FromContextPart};
14pub use super::common::{Extension, RequestId};
15use crate::{
16    RoleServer,
17    handler::server::wrapper::Parameters,
18    model::{GetPromptResponse, GetPromptResult, InputRequiredResult, PromptMessage},
19    service::{MaybeBoxFuture, MaybeSend, MaybeSendFuture, RequestContext},
20};
21
22/// Context for prompt retrieval operations
23#[non_exhaustive]
24pub struct PromptContext<'a, S> {
25    pub server: &'a S,
26    pub name: String,
27    pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
28    pub context: RequestContext<RoleServer>,
29}
30
31impl<'a, S> PromptContext<'a, S> {
32    pub fn new(
33        server: &'a S,
34        name: String,
35        arguments: Option<serde_json::Map<String, serde_json::Value>>,
36        context: RequestContext<RoleServer>,
37    ) -> Self {
38        Self {
39            server,
40            name,
41            arguments,
42            context,
43        }
44    }
45}
46
47impl<S> AsRequestContext for PromptContext<'_, S> {
48    fn as_request_context(&self) -> &RequestContext<RoleServer> {
49        &self.context
50    }
51
52    fn as_request_context_mut(&mut self) -> &mut RequestContext<RoleServer> {
53        &mut self.context
54    }
55}
56
57/// Trait for handling prompt retrieval
58pub trait GetPromptHandler<S, A> {
59    fn handle(
60        self,
61        context: PromptContext<'_, S>,
62    ) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>;
63}
64
65/// Type alias for dynamic prompt handlers
66#[cfg(not(feature = "local"))]
67pub type DynGetPromptHandler<S> = dyn for<'a> Fn(PromptContext<'a, S>) -> BoxFuture<'a, Result<GetPromptResponse, crate::ErrorData>>
68    + Send
69    + Sync;
70
71#[cfg(feature = "local")]
72pub type DynGetPromptHandler<S> = dyn for<'a> Fn(
73    PromptContext<'a, S>,
74) -> futures::future::LocalBoxFuture<
75    'a,
76    Result<GetPromptResponse, crate::ErrorData>,
77>;
78
79/// Adapter type for async methods that return `Vec<PromptMessage>`
80pub struct AsyncMethodAdapter<T>(PhantomData<T>);
81
82/// Adapter type for async methods with parameters that return `Vec<PromptMessage>`
83pub struct AsyncMethodWithArgsAdapter<T>(PhantomData<T>);
84
85/// Adapter types for macro-generated implementations
86#[allow(clippy::type_complexity)]
87pub struct AsyncPromptAdapter<P, Fut, R>(PhantomData<fn(P) -> fn(Fut) -> R>);
88pub struct SyncPromptAdapter<P, R>(PhantomData<fn(P) -> R>);
89pub struct AsyncPromptMethodAdapter<P, R>(PhantomData<fn(P) -> R>);
90pub struct SyncPromptMethodAdapter<P, R>(PhantomData<fn(P) -> R>);
91
92/// Trait for types that can be converted into GetPromptResult
93pub trait IntoGetPromptResult {
94    fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData>;
95}
96
97impl IntoGetPromptResult for GetPromptResult {
98    fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
99        Ok(self.into())
100    }
101}
102
103impl IntoGetPromptResult for InputRequiredResult {
104    fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
105        Ok(self.into())
106    }
107}
108
109impl IntoGetPromptResult for Vec<PromptMessage> {
110    fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
111        Ok(GetPromptResult::new(self).into())
112    }
113}
114
115impl<T: IntoGetPromptResult> IntoGetPromptResult for Result<T, crate::ErrorData> {
116    fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
117        self.and_then(|v| v.into_get_prompt_result())
118    }
119}
120
121// Future wrapper that automatically handles IntoGetPromptResult conversion
122pin_project_lite::pin_project! {
123    #[project = IntoGetPromptResultFutProj]
124    #[non_exhaustive]
125    pub enum IntoGetPromptResultFut<F, R> {
126        Pending {
127            #[pin]
128            fut: F,
129            _marker: PhantomData<R>,
130        },
131        Ready {
132            #[pin]
133            result: futures::future::Ready<Result<GetPromptResponse, crate::ErrorData>>,
134        }
135    }
136}
137
138impl<F, R> Future for IntoGetPromptResultFut<F, R>
139where
140    F: Future<Output = R>,
141    R: IntoGetPromptResult,
142{
143    type Output = Result<GetPromptResponse, crate::ErrorData>;
144
145    fn poll(
146        self: std::pin::Pin<&mut Self>,
147        cx: &mut std::task::Context<'_>,
148    ) -> std::task::Poll<Self::Output> {
149        match self.project() {
150            IntoGetPromptResultFutProj::Pending { fut, _marker } => fut
151                .poll(cx)
152                .map(IntoGetPromptResult::into_get_prompt_result),
153            IntoGetPromptResultFutProj::Ready { result } => result.poll(cx),
154        }
155    }
156}
157
158// Prompt-specific extractor for prompt name
159#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
160pub struct PromptName(pub String);
161
162impl<S> FromContextPart<PromptContext<'_, S>> for PromptName {
163    fn from_context_part(context: &mut PromptContext<S>) -> Result<Self, crate::ErrorData> {
164        Ok(Self(context.name.clone()))
165    }
166}
167
168// Special implementation for Parameters that handles prompt arguments
169impl<S, P> FromContextPart<PromptContext<'_, S>> for Parameters<P>
170where
171    P: DeserializeOwned,
172{
173    fn from_context_part(context: &mut PromptContext<S>) -> Result<Self, crate::ErrorData> {
174        let params = if let Some(args_map) = context.arguments.take() {
175            let args_value = serde_json::Value::Object(args_map);
176            serde_json::from_value::<P>(args_value).map_err(|e| {
177                crate::ErrorData::invalid_params(format!("Failed to parse parameters: {}", e), None)
178            })?
179        } else {
180            // Try to deserialize from empty object for optional fields
181            serde_json::from_value::<P>(serde_json::json!({})).map_err(|e| {
182                crate::ErrorData::invalid_params(
183                    format!("Missing required parameters: {}", e),
184                    None,
185                )
186            })?
187        };
188        Ok(Parameters(params))
189    }
190}
191
192// Macro to generate GetPromptHandler implementations for various parameter combinations
193macro_rules! impl_prompt_handler_for {
194    ($($T: ident)*) => {
195        impl_prompt_handler_for!([] [$($T)*]);
196    };
197    // finished
198    ([$($Tn: ident)*] []) => {
199        impl_prompt_handler_for!(@impl $($Tn)*);
200    };
201    ([$($Tn: ident)*] [$Tn_1: ident $($Rest: ident)*]) => {
202        impl_prompt_handler_for!(@impl $($Tn)*);
203        impl_prompt_handler_for!([$($Tn)* $Tn_1] [$($Rest)*]);
204    };
205    (@impl $($Tn: ident)*) => {
206        // Implementation for async methods (transformed by #[prompt] macro)
207        impl<$($Tn,)* S, F, R> GetPromptHandler<S, ($($Tn,)*)> for F
208        where
209            $(
210                $Tn: for<'a> FromContextPart<PromptContext<'a, S>> + MaybeSendFuture,
211            )*
212            F: FnOnce(&S, $($Tn,)*) -> MaybeBoxFuture<'_, R> + MaybeSendFuture,
213            R: IntoGetPromptResult + MaybeSendFuture + 'static,
214            S: MaybeSend + 'static,
215        {
216            #[allow(unused_variables, non_snake_case, unused_mut)]
217            fn handle(
218                self,
219                mut context: PromptContext<'_, S>,
220            ) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
221            {
222                $(
223                    let result = $Tn::from_context_part(&mut context);
224                    let $Tn = match result {
225                        Ok(value) => value,
226                        Err(e) => return Box::pin(std::future::ready(Err(e))),
227                    };
228                )*
229                let service = context.server;
230                let fut = self(service, $($Tn,)*);
231                Box::pin(async move {
232                    let result = fut.await;
233                    result.into_get_prompt_result()
234                })
235            }
236        }
237
238
239        // Implementation for sync methods
240        impl<$($Tn,)* S, F, R> GetPromptHandler<S, SyncPromptMethodAdapter<($($Tn,)*), R>> for F
241        where
242            $(
243                $Tn: for<'a> FromContextPart<PromptContext<'a, S>> + MaybeSendFuture,
244            )*
245            F: FnOnce(&S, $($Tn,)*) -> R + MaybeSendFuture,
246            R: IntoGetPromptResult + MaybeSendFuture,
247            S: MaybeSend,
248        {
249            #[allow(unused_variables, non_snake_case, unused_mut)]
250            fn handle(
251                self,
252                mut context: PromptContext<'_, S>,
253            ) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
254            {
255                $(
256                    let result = $Tn::from_context_part(&mut context);
257                    let $Tn = match result {
258                        Ok(value) => value,
259                        Err(e) => return Box::pin(std::future::ready(Err(e))),
260                    };
261                )*
262                let service = context.server;
263                let result = self(service, $($Tn,)*);
264                Box::pin(std::future::ready(result.into_get_prompt_result()))
265            }
266        }
267
268
269        // AsyncPromptAdapter - for standalone functions returning GetPromptResult
270        impl<$($Tn,)* S, F, Fut, R> GetPromptHandler<S, AsyncPromptAdapter<($($Tn,)*), Fut, R>> for F
271        where
272            $(
273                $Tn: for<'a> FromContextPart<PromptContext<'a, S>> + MaybeSendFuture + 'static,
274            )*
275            F: FnOnce($($Tn,)*) -> Fut + MaybeSendFuture + 'static,
276            Fut: Future<Output = Result<R, crate::ErrorData>> + MaybeSendFuture + 'static,
277            R: IntoGetPromptResult + MaybeSendFuture + 'static,
278            S: MaybeSend + 'static,
279        {
280            #[allow(unused_variables, non_snake_case, unused_mut)]
281            fn handle(
282                self,
283                mut context: PromptContext<'_, S>,
284            ) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
285            {
286                // Extract all parameters before moving into the async block
287                $(
288                    let result = $Tn::from_context_part(&mut context);
289                    let $Tn = match result {
290                        Ok(value) => value,
291                        Err(e) => return Box::pin(std::future::ready(Err(e))),
292                    };
293                )*
294
295                // Since we're dealing with standalone functions that don't take &S,
296                // we can return a 'static future
297                Box::pin(async move {
298                    let result = self($($Tn,)*).await?;
299                    result.into_get_prompt_result()
300                })
301            }
302        }
303
304
305        // SyncPromptAdapter - for standalone sync functions returning Result
306        impl<$($Tn,)* S, F, R> GetPromptHandler<S, SyncPromptAdapter<($($Tn,)*), R>> for F
307        where
308            $(
309                $Tn: for<'a> FromContextPart<PromptContext<'a, S>> + MaybeSendFuture + 'static,
310            )*
311            F: FnOnce($($Tn,)*) -> Result<R, crate::ErrorData> + MaybeSendFuture + 'static,
312            R: IntoGetPromptResult + MaybeSendFuture + 'static,
313            S: MaybeSend,
314        {
315            #[allow(unused_variables, non_snake_case, unused_mut)]
316            fn handle(
317                self,
318                mut context: PromptContext<'_, S>,
319            ) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
320            {
321                $(
322                    let result = $Tn::from_context_part(&mut context);
323                    let $Tn = match result {
324                        Ok(value) => value,
325                        Err(e) => return Box::pin(std::future::ready(Err(e))),
326                    };
327                )*
328                let result = self($($Tn,)*);
329                Box::pin(std::future::ready(result.and_then(|r| r.into_get_prompt_result())))
330            }
331        }
332
333    };
334}
335
336// Invoke the macro to generate implementations for up to 16 parameters
337impl_prompt_handler_for!(T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15);
338
339/// Extract prompt arguments from a type's JSON schema
340/// This function analyzes the schema of a type and extracts the properties
341/// as PromptArgument entries with name, description, and required status
342pub fn cached_arguments_from_schema<T: schemars::JsonSchema + std::any::Any>()
343-> Option<Vec<crate::model::PromptArgument>> {
344    let schema = super::common::schema_for_type::<T>();
345    let schema_value = serde_json::Value::Object((*schema).clone());
346
347    let properties = schema_value.get("properties").and_then(|p| p.as_object());
348
349    if let Some(props) = properties {
350        let required = schema_value
351            .get("required")
352            .and_then(|r| r.as_array())
353            .map(|arr| {
354                arr.iter()
355                    .filter_map(|v| v.as_str())
356                    .collect::<std::collections::HashSet<_>>()
357            })
358            .unwrap_or_default();
359
360        let mut arguments = Vec::new();
361        for (name, prop_schema) in props {
362            let description = prop_schema
363                .get("description")
364                .and_then(|d| d.as_str())
365                .map(|s| s.to_string());
366
367            arguments.push(crate::model::PromptArgument {
368                name: name.clone(),
369                title: None,
370                description,
371                required: Some(required.contains(name.as_str())),
372            });
373        }
374
375        if arguments.is_empty() {
376            None
377        } else {
378            Some(arguments)
379        }
380    } else {
381        None
382    }
383}