mcpkit_server/capability/
prompts.rs1use crate::context::Context;
7use crate::handler::PromptHandler;
8use mcpkit_core::error::McpError;
9use mcpkit_core::types::prompt::{GetPromptResult, Prompt, PromptArgument, PromptMessage};
10use serde_json::Value;
11use std::collections::HashMap;
12use std::future::Future;
13use std::pin::Pin;
14
15pub type BoxedPromptFn = Box<
17 dyn for<'a> Fn(
18 Option<Value>,
19 &'a Context<'a>,
20 )
21 -> Pin<Box<dyn Future<Output = Result<GetPromptResult, McpError>> + Send + 'a>>
22 + Send
23 + Sync,
24>;
25
26pub struct RegisteredPrompt {
28 pub prompt: Prompt,
30 pub handler: BoxedPromptFn,
32}
33
34pub struct PromptService {
39 prompts: HashMap<String, RegisteredPrompt>,
40}
41
42impl Default for PromptService {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl PromptService {
49 #[must_use]
51 pub fn new() -> Self {
52 Self {
53 prompts: HashMap::new(),
54 }
55 }
56
57 pub fn register<F, Fut>(&mut self, prompt: Prompt, handler: F)
59 where
60 F: Fn(Option<Value>, &Context<'_>) -> Fut + Send + Sync + 'static,
61 Fut: Future<Output = Result<GetPromptResult, McpError>> + Send + 'static,
62 {
63 let name = prompt.name.clone();
64 let boxed: BoxedPromptFn = Box::new(move |args, ctx| Box::pin(handler(args, ctx)));
65 self.prompts.insert(
66 name,
67 RegisteredPrompt {
68 prompt,
69 handler: boxed,
70 },
71 );
72 }
73
74 #[must_use]
76 pub fn get(&self, name: &str) -> Option<&RegisteredPrompt> {
77 self.prompts.get(name)
78 }
79
80 #[must_use]
82 pub fn contains(&self, name: &str) -> bool {
83 self.prompts.contains_key(name)
84 }
85
86 #[must_use]
88 pub fn list(&self) -> Vec<&Prompt> {
89 self.prompts.values().map(|r| &r.prompt).collect()
90 }
91
92 #[must_use]
94 pub fn len(&self) -> usize {
95 self.prompts.len()
96 }
97
98 #[must_use]
100 pub fn is_empty(&self) -> bool {
101 self.prompts.is_empty()
102 }
103
104 pub async fn render(
106 &self,
107 name: &str,
108 arguments: Option<Value>,
109 ctx: &Context<'_>,
110 ) -> Result<GetPromptResult, McpError> {
111 let registered = self.prompts.get(name).ok_or_else(|| {
112 McpError::invalid_params("prompts/get", format!("Unknown prompt: {name}"))
113 })?;
114
115 (registered.handler)(arguments, ctx).await
116 }
117}
118
119impl PromptHandler for PromptService {
120 async fn list_prompts(&self, _ctx: &Context<'_>) -> Result<Vec<Prompt>, McpError> {
121 Ok(self.list().into_iter().cloned().collect())
122 }
123
124 async fn get_prompt(
125 &self,
126 name: &str,
127 arguments: Option<serde_json::Map<String, Value>>,
128 ctx: &Context<'_>,
129 ) -> Result<GetPromptResult, McpError> {
130 let args = arguments.map(Value::Object);
131 self.render(name, args, ctx).await
132 }
133}
134
135pub struct PromptBuilder {
137 name: String,
138 description: Option<String>,
139 arguments: Vec<PromptArgument>,
140}
141
142impl PromptBuilder {
143 pub fn new(name: impl Into<String>) -> Self {
145 Self {
146 name: name.into(),
147 description: None,
148 arguments: Vec::new(),
149 }
150 }
151
152 pub fn description(mut self, desc: impl Into<String>) -> Self {
154 self.description = Some(desc.into());
155 self
156 }
157
158 pub fn required_arg(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
160 self.arguments.push(PromptArgument {
161 name: name.into(),
162 title: None,
163 description: Some(description.into()),
164 required: Some(true),
165 });
166 self
167 }
168
169 pub fn optional_arg(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
171 self.arguments.push(PromptArgument {
172 name: name.into(),
173 title: None,
174 description: Some(description.into()),
175 required: Some(false),
176 });
177 self
178 }
179
180 #[must_use]
182 pub fn argument(mut self, arg: PromptArgument) -> Self {
183 self.arguments.push(arg);
184 self
185 }
186
187 #[must_use]
189 pub fn build(self) -> Prompt {
190 Prompt {
191 name: self.name,
192 title: None,
193 description: self.description,
194 icons: None,
195 arguments: if self.arguments.is_empty() {
196 None
197 } else {
198 Some(self.arguments)
199 },
200 meta: None,
201 }
202 }
203}
204
205pub struct PromptResultBuilder {
207 description: Option<String>,
208 messages: Vec<PromptMessage>,
209}
210
211impl Default for PromptResultBuilder {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl PromptResultBuilder {
218 #[must_use]
220 pub const fn new() -> Self {
221 Self {
222 description: None,
223 messages: Vec::new(),
224 }
225 }
226
227 pub fn description(mut self, desc: impl Into<String>) -> Self {
229 self.description = Some(desc.into());
230 self
231 }
232
233 pub fn user_text(mut self, text: impl Into<String>) -> Self {
235 self.messages.push(PromptMessage::user(text.into()));
236 self
237 }
238
239 pub fn assistant_text(mut self, text: impl Into<String>) -> Self {
241 self.messages.push(PromptMessage::assistant(text.into()));
242 self
243 }
244
245 #[must_use]
247 pub fn message(mut self, msg: PromptMessage) -> Self {
248 self.messages.push(msg);
249 self
250 }
251
252 #[must_use]
254 pub fn build(self) -> GetPromptResult {
255 GetPromptResult {
256 description: self.description,
257 messages: self.messages,
258 meta: None,
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::context::{Context, NoOpPeer};
267 use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
268 use mcpkit_core::protocol::RequestId;
269 use mcpkit_core::protocol_version::ProtocolVersion;
270
271 fn make_context() -> (
272 RequestId,
273 ClientCapabilities,
274 ServerCapabilities,
275 ProtocolVersion,
276 NoOpPeer,
277 ) {
278 (
279 RequestId::Number(1),
280 ClientCapabilities::default(),
281 ServerCapabilities::default(),
282 ProtocolVersion::LATEST,
283 NoOpPeer,
284 )
285 }
286
287 #[test]
288 fn test_prompt_builder() {
289 let prompt = PromptBuilder::new("code-review")
290 .description("Review code for issues")
291 .required_arg("code", "The code to review")
292 .optional_arg("language", "Programming language")
293 .build();
294
295 assert_eq!(prompt.name, "code-review");
296 assert_eq!(
297 prompt.description.as_deref(),
298 Some("Review code for issues")
299 );
300 assert_eq!(prompt.arguments.as_ref().map(std::vec::Vec::len), Some(2));
301 }
302
303 #[test]
304 fn test_prompt_result_builder() {
305 let result = PromptResultBuilder::new()
306 .description("Generated review")
307 .user_text("Please review this code")
308 .assistant_text("I'll analyze the code...")
309 .build();
310
311 assert_eq!(result.description.as_deref(), Some("Generated review"));
312 assert_eq!(result.messages.len(), 2);
313 }
314
315 #[tokio::test]
316 async fn test_prompt_service() -> Result<(), Box<dyn std::error::Error>> {
317 let mut service = PromptService::new();
318
319 let prompt = PromptBuilder::new("greeting")
320 .description("Generate a greeting")
321 .required_arg("name", "Name to greet")
322 .build();
323
324 service.register(prompt, |args, _ctx| async move {
325 let name = args
326 .and_then(|v| v.get("name").and_then(|n| n.as_str()).map(String::from))
327 .unwrap_or_else(|| "World".to_string());
328
329 Ok(PromptResultBuilder::new()
330 .user_text(format!("Generate a greeting for {name}"))
331 .build())
332 });
333
334 assert!(service.contains("greeting"));
335 assert_eq!(service.len(), 1);
336
337 let (req_id, client_caps, server_caps, protocol_version, peer) = make_context();
338 let ctx = Context::new(
339 &req_id,
340 None,
341 &client_caps,
342 &server_caps,
343 protocol_version,
344 &peer,
345 );
346
347 let result = service
348 .render("greeting", Some(serde_json::json!({"name": "Alice"})), &ctx)
349 .await?;
350
351 assert!(!result.messages.is_empty());
352
353 Ok(())
354 }
355}