Skip to main content

pmcp_server/prompts/
workflows.rs

1//! Guided workflow prompt handlers.
2//!
3//! Each struct implements `PromptHandler` with metadata describing its
4//! name, description, and argument schema. Prompts return actionable
5//! guidance for common MCP development scenarios.
6
7use async_trait::async_trait;
8use pmcp::types::{Content, GetPromptResult, PromptArgument, PromptInfo, PromptMessage, Role};
9use pmcp::RequestHandlerExtra;
10use std::collections::HashMap;
11
12// ---------------------------------------------------------------------------
13// Helper
14// ---------------------------------------------------------------------------
15
16/// Build a single-message assistant prompt result.
17fn assistant_result(description: &str, text: String) -> pmcp::Result<GetPromptResult> {
18    Ok(GetPromptResult::new(
19        vec![PromptMessage {
20            role: Role::Assistant,
21            content: Content::Text { text },
22        }],
23        Some(description.to_string()),
24    ))
25}
26
27/// Build a prompt argument descriptor.
28fn arg(name: &str, description: &str, required: bool) -> PromptArgument {
29    PromptArgument {
30        name: name.to_string(),
31        description: Some(description.to_string()),
32        required,
33        completion: None,
34        arg_type: None,
35    }
36}
37
38// ---------------------------------------------------------------------------
39// 1. QuickstartPrompt
40// ---------------------------------------------------------------------------
41
42/// Step-by-step guide to create a first PMCP server.
43pub struct QuickstartPrompt;
44
45#[async_trait]
46impl pmcp::server::PromptHandler for QuickstartPrompt {
47    async fn handle(
48        &self,
49        _args: HashMap<String, String>,
50        _extra: RequestHandlerExtra,
51    ) -> pmcp::Result<GetPromptResult> {
52        assistant_result(
53            "Quickstart guide for building your first PMCP server",
54            QUICKSTART_CONTENT.to_string(),
55        )
56    }
57
58    fn metadata(&self) -> Option<PromptInfo> {
59        Some(PromptInfo {
60            name: "quickstart".to_string(),
61            description: Some(
62                "Step-by-step guide to create your first PMCP MCP server".to_string(),
63            ),
64            arguments: None,
65        })
66    }
67}
68
69const QUICKSTART_CONTENT: &str = "\
70# PMCP Quickstart
71
72## 1. Create a new project
73
74```bash
75cargo pmcp init my-server
76cd my-server
77```
78
79## 2. Add a tool
80
81Open `src/main.rs` and add a typed tool:
82
83```rust
84use pmcp::server::TypedSyncTool;
85use serde::Deserialize;
86
87#[derive(Debug, Deserialize)]
88struct GreetInput { name: String }
89
90struct GreetTool;
91
92impl TypedSyncTool for GreetTool {
93    type Input = GreetInput;
94    fn metadata(&self) -> pmcp::types::ToolInfo {
95        pmcp::types::ToolInfo::new(\"greet\", \"Greet someone\")
96    }
97    fn call_sync(&self, input: Self::Input, _extra: pmcp::RequestHandlerExtra)
98        -> pmcp::Result<pmcp::CallToolResult> {
99        Ok(pmcp::CallToolResult::text(format!(\"Hello, {}!\", input.name)))
100    }
101}
102```
103
104## 3. Register and run
105
106```rust
107let server = pmcp::Server::builder()
108    .name(\"my-server\")
109    .version(\"0.1.0\")
110    .tool_typed(GreetTool)
111    .build()?;
112```
113
114## 4. Test
115
116```bash
117cargo pmcp test check http://localhost:8080
118```";
119
120// ---------------------------------------------------------------------------
121// 2. CreateMcpServerPrompt
122// ---------------------------------------------------------------------------
123
124/// Guided workspace setup for a new MCP server.
125pub struct CreateMcpServerPrompt;
126
127#[async_trait]
128impl pmcp::server::PromptHandler for CreateMcpServerPrompt {
129    async fn handle(
130        &self,
131        args: HashMap<String, String>,
132        _extra: RequestHandlerExtra,
133    ) -> pmcp::Result<GetPromptResult> {
134        let name = args
135            .get("name")
136            .ok_or_else(|| pmcp::Error::validation("Required argument 'name' is missing"))?;
137        let template = args
138            .get("template")
139            .map(String::as_str)
140            .unwrap_or("minimal");
141        let text = format!(
142            "# Create MCP Server: {name}\n\n\
143             ## Setup\n\n\
144             ```bash\n\
145             cargo pmcp init {name} --template {template}\n\
146             cd {name}\n\
147             ```\n\n\
148             ## Project structure\n\n\
149             ```\n\
150             {name}/\n\
151               Cargo.toml\n\
152               src/\n\
153                 main.rs       # Server entry point\n\
154                 tools/        # Tool implementations\n\
155                 resources/    # Resource handlers\n\
156                 prompts/      # Prompt handlers\n\
157             ```\n\n\
158             ## Next steps\n\n\
159             1. Add tools with `cargo pmcp scaffold tool <name>`\n\
160             2. Run with `cargo run`\n\
161             3. Test with `cargo pmcp test check http://localhost:8080`\n"
162        );
163        assistant_result("Workspace setup instructions", text)
164    }
165
166    fn metadata(&self) -> Option<PromptInfo> {
167        Some(PromptInfo {
168            name: "create-mcp-server".to_string(),
169            description: Some("Set up a new PMCP MCP server workspace".to_string()),
170            arguments: Some(vec![
171                arg("name", "Server project name", true),
172                arg(
173                    "template",
174                    "Template: minimal or calculator (default: minimal)",
175                    false,
176                ),
177            ]),
178        })
179    }
180}
181
182// ---------------------------------------------------------------------------
183// 3. AddToolPrompt
184// ---------------------------------------------------------------------------
185
186/// Guide to adding a new tool to an existing server.
187pub struct AddToolPrompt;
188
189#[async_trait]
190impl pmcp::server::PromptHandler for AddToolPrompt {
191    async fn handle(
192        &self,
193        args: HashMap<String, String>,
194        _extra: RequestHandlerExtra,
195    ) -> pmcp::Result<GetPromptResult> {
196        let tool_name = args
197            .get("tool_name")
198            .ok_or_else(|| pmcp::Error::validation("Required argument 'tool_name' is missing"))?;
199        let desc = args
200            .get("description")
201            .cloned()
202            .unwrap_or_else(|| format!("A {tool_name} tool"));
203        let text = format!(
204            "# Add Tool: {tool_name}\n\n\
205             ## 1. Create the tool struct\n\n\
206             ```rust\n\
207             use pmcp::server::TypedTool;\n\
208             use serde::Deserialize;\n\n\
209             #[derive(Debug, Deserialize)]\n\
210             struct {pascal}Input {{\n\
211                 // Add input fields here\n\
212             }}\n\n\
213             struct {pascal}Tool;\n\n\
214             #[async_trait::async_trait]\n\
215             impl TypedTool for {pascal}Tool {{\n\
216                 type Input = {pascal}Input;\n\n\
217                 fn metadata(&self) -> pmcp::types::ToolInfo {{\n\
218                     pmcp::types::ToolInfo::new(\"{tool_name}\", \"{desc}\")\n\
219                 }}\n\n\
220                 async fn call(&self, input: Self::Input, _extra: pmcp::RequestHandlerExtra)\n\
221                     -> pmcp::Result<pmcp::CallToolResult> {{\n\
222                     // Implement tool logic\n\
223                     Ok(pmcp::CallToolResult::text(\"result\"))\n\
224                 }}\n\
225             }}\n\
226             ```\n\n\
227             ## 2. Register\n\n\
228             ```rust\n\
229             server_builder.tool_typed({pascal}Tool);\n\
230             ```\n\n\
231             ## 3. Test\n\n\
232             ```bash\n\
233             cargo pmcp test check http://localhost:8080\n\
234             ```\n",
235            pascal = crate::util::to_pascal_case(tool_name),
236        );
237        assistant_result("Tool creation guide", text)
238    }
239
240    fn metadata(&self) -> Option<PromptInfo> {
241        Some(PromptInfo {
242            name: "add-tool".to_string(),
243            description: Some("Add a new tool to an existing PMCP server".to_string()),
244            arguments: Some(vec![
245                arg("tool_name", "Name for the new tool (snake_case)", true),
246                arg("description", "Human-readable tool description", false),
247            ]),
248        })
249    }
250}
251
252// ---------------------------------------------------------------------------
253// 4. DiagnosePrompt
254// ---------------------------------------------------------------------------
255
256/// Diagnostic steps for a running MCP server.
257pub struct DiagnosePrompt;
258
259#[async_trait]
260impl pmcp::server::PromptHandler for DiagnosePrompt {
261    async fn handle(
262        &self,
263        args: HashMap<String, String>,
264        _extra: RequestHandlerExtra,
265    ) -> pmcp::Result<GetPromptResult> {
266        let server_url = args
267            .get("server_url")
268            .ok_or_else(|| pmcp::Error::validation("Required argument 'server_url' is missing"))?;
269        let text = format!(
270            "# Diagnose Server: {server_url}\n\n\
271             ## Step 1: Check connectivity\n\n\
272             Use the `test_check` tool to run protocol compliance checks:\n\n\
273             ```\n\
274             test_check(url: \"{server_url}\")\n\
275             ```\n\n\
276             ## Step 2: Verify tool listing\n\n\
277             The test_check tool will validate:\n\
278             - Server responds to `initialize`\n\
279             - `tools/list` returns valid tool schemas\n\
280             - `resources/list` returns valid resource info\n\
281             - `prompts/list` returns valid prompt metadata\n\n\
282             ## Step 3: Common issues\n\n\
283             - **Connection refused**: Server not running or wrong port\n\
284             - **Timeout**: Server is slow to respond, check for blocking operations\n\
285             - **Invalid schema**: Tool input schemas must be valid JSON Schema\n\
286             - **Missing capabilities**: Ensure server advertises the capabilities it supports\n\n\
287             ## Step 4: Generate test scenarios\n\n\
288             ```\n\
289             test_generate(url: \"{server_url}\")\n\
290             ```\n\n\
291             This generates test cases based on the server's actual tool/resource listing.\n"
292        );
293        assistant_result("Server diagnostic steps", text)
294    }
295
296    fn metadata(&self) -> Option<PromptInfo> {
297        Some(PromptInfo {
298            name: "diagnose".to_string(),
299            description: Some("Diagnose issues with a running MCP server".to_string()),
300            arguments: Some(vec![arg(
301                "server_url",
302                "URL of the MCP server to diagnose",
303                true,
304            )]),
305        })
306    }
307}
308
309// ---------------------------------------------------------------------------
310// 5. SetupAuthPrompt
311// ---------------------------------------------------------------------------
312
313/// Auth configuration guide.
314pub struct SetupAuthPrompt;
315
316#[async_trait]
317impl pmcp::server::PromptHandler for SetupAuthPrompt {
318    async fn handle(
319        &self,
320        args: HashMap<String, String>,
321        _extra: RequestHandlerExtra,
322    ) -> pmcp::Result<GetPromptResult> {
323        let auth_type = args.get("auth_type").map(String::as_str).unwrap_or("oauth");
324        let text = match auth_type {
325            "api-key" => AUTH_API_KEY_CONTENT.to_string(),
326            "jwt" => AUTH_JWT_CONTENT.to_string(),
327            _ => AUTH_OAUTH_CONTENT.to_string(),
328        };
329        assistant_result("Authentication setup guide", text)
330    }
331
332    fn metadata(&self) -> Option<PromptInfo> {
333        Some(PromptInfo {
334            name: "setup-auth".to_string(),
335            description: Some("Configure authentication for your MCP server".to_string()),
336            arguments: Some(vec![arg(
337                "auth_type",
338                "Auth type: oauth, api-key, or jwt (default: oauth)",
339                false,
340            )]),
341        })
342    }
343}
344
345const AUTH_OAUTH_CONTENT: &str = "\
346# OAuth 2.0 Setup
347
348## 1. Add OAuth middleware
349
350```rust
351use pmcp::server::auth::{OAuthMiddleware, OAuthConfig};
352
353let config = OAuthConfig {
354    issuer_url: \"https://auth.example.com\".into(),
355    audience: Some(\"my-api\".into()),
356    ..Default::default()
357};
358
359server_builder.middleware(OAuthMiddleware::new(config));
360```
361
362## 2. Configure your OAuth provider
363
364Set up a client application in your OAuth provider (Auth0, Okta, etc.)
365and note the issuer URL and audience.
366
367## 3. Test with a token
368
369```bash
370curl -H 'Authorization: Bearer <token>' http://localhost:8080/mcp
371```";
372
373const AUTH_API_KEY_CONTENT: &str = "\
374# API Key Setup
375
376## 1. Add API key middleware
377
378```rust
379use pmcp::server::auth::ApiKeyMiddleware;
380
381let middleware = ApiKeyMiddleware::new(\"X-API-Key\", vec![
382    \"sk-your-key-here\".into(),
383]);
384server_builder.middleware(middleware);
385```
386
387## 2. Store keys securely
388
389```bash
390cargo pmcp secret set API_KEY=sk-your-key-here
391```
392
393## 3. Test
394
395```bash
396curl -H 'X-API-Key: sk-your-key-here' http://localhost:8080/mcp
397```";
398
399const AUTH_JWT_CONTENT: &str = "\
400# JWT Setup
401
402## 1. Add JWT middleware
403
404```rust
405use pmcp::server::auth::JwtMiddleware;
406
407let jwt = JwtMiddleware::builder()
408    .issuer(\"https://auth.example.com\")
409    .audience(\"my-api\")
410    .jwks_url(\"https://auth.example.com/.well-known/jwks.json\")
411    .build()?;
412server_builder.middleware(jwt);
413```
414
415## 2. Token validation
416
417The middleware validates:
418- Token signature (via JWKS)
419- Issuer claim
420- Audience claim
421- Expiration
422
423## 3. Test
424
425```bash
426curl -H 'Authorization: Bearer <jwt-token>' http://localhost:8080/mcp
427```";
428
429// ---------------------------------------------------------------------------
430// 6. DebugProtocolErrorPrompt
431// ---------------------------------------------------------------------------
432
433/// Protocol error debugging steps.
434pub struct DebugProtocolErrorPrompt;
435
436#[async_trait]
437impl pmcp::server::PromptHandler for DebugProtocolErrorPrompt {
438    async fn handle(
439        &self,
440        args: HashMap<String, String>,
441        _extra: RequestHandlerExtra,
442    ) -> pmcp::Result<GetPromptResult> {
443        let error_msg = args
444            .get("error_message")
445            .cloned()
446            .unwrap_or_else(|| "(no error message provided)".into());
447        let text = format!(
448            "# Debug Protocol Error\n\n\
449             **Error:** {error_msg}\n\n\
450             ## Common Causes\n\n\
451             ### JSON-RPC Errors\n\
452             - **-32700 Parse Error**: Invalid JSON in request body\n\
453             - **-32600 Invalid Request**: Missing `jsonrpc`, `method`, or `id` fields\n\
454             - **-32601 Method Not Found**: Calling an unregistered method\n\
455             - **-32602 Invalid Params**: Tool input doesn't match schema\n\
456             - **-32603 Internal Error**: Server threw an unhandled exception\n\n\
457             ## Debugging Steps\n\n\
458             1. **Enable verbose logging**: Set `RUST_LOG=debug` on the server\n\
459             2. **Capture the raw request**: Use `cargo pmcp connect <url>` for interactive inspection\n\
460             3. **Validate schemas**: Run `cargo pmcp schema export <url>` to check tool schemas\n\
461             4. **Run compliance tests**: Use `cargo pmcp test check <url>` for protocol validation\n\n\
462             ## Common Fixes\n\n\
463             - Ensure all tool input types derive `Deserialize`\n\
464             - Check that `required` fields in JSON Schema match Rust struct fields\n\
465             - Verify the server advertises correct capabilities in `initialize` response\n\
466             - Check content-type headers (`application/json` for JSON-RPC)\n"
467        );
468        assistant_result("Protocol error debugging guide", text)
469    }
470
471    fn metadata(&self) -> Option<PromptInfo> {
472        Some(PromptInfo {
473            name: "debug-protocol-error".to_string(),
474            description: Some("Debug MCP protocol errors".to_string()),
475            arguments: Some(vec![arg(
476                "error_message",
477                "The error message to diagnose",
478                false,
479            )]),
480        })
481    }
482}
483
484// ---------------------------------------------------------------------------
485// 7. MigratePrompt
486// ---------------------------------------------------------------------------
487
488/// Migration guide from TypeScript SDK to PMCP.
489pub struct MigratePrompt;
490
491#[async_trait]
492impl pmcp::server::PromptHandler for MigratePrompt {
493    async fn handle(
494        &self,
495        _args: HashMap<String, String>,
496        _extra: RequestHandlerExtra,
497    ) -> pmcp::Result<GetPromptResult> {
498        assistant_result(
499            "Migration guide from TypeScript SDK to PMCP",
500            MIGRATE_CONTENT.to_string(),
501        )
502    }
503
504    fn metadata(&self) -> Option<PromptInfo> {
505        Some(PromptInfo {
506            name: "migrate".to_string(),
507            description: Some(
508                "Guide for migrating from TypeScript MCP SDK to PMCP (Rust)".to_string(),
509            ),
510            arguments: None,
511        })
512    }
513}
514
515const MIGRATE_CONTENT: &str = "\
516# Migrate from TypeScript SDK to PMCP
517
518## Concept Mapping
519
520| TypeScript SDK         | PMCP (Rust)                  |
521|------------------------|------------------------------|
522| `server.tool()`        | `TypedTool` / `TypedSyncTool`|
523| `server.resource()`    | `ResourceHandler` trait      |
524| `server.prompt()`      | `PromptHandler` trait        |
525| `zod` schemas          | `serde::Deserialize` structs |
526| `McpError`             | `pmcp::Error` variants       |
527| `StdioServerTransport` | `StdioTransport`             |
528| `SSEServerTransport`   | `StreamableHttpServer`       |
529
530## Migration Steps
531
532### 1. Create Rust project
533```bash
534cargo pmcp init my-server
535```
536
537### 2. Convert tool definitions
538TypeScript:
539```typescript
540server.tool('greet', { name: z.string() }, async ({ name }) => ({
541    content: [{ type: 'text', text: `Hello ${name}` }]
542}));
543```
544
545Rust:
546```rust
547#[derive(Deserialize)]
548struct GreetInput { name: String }
549
550struct GreetTool;
551impl TypedSyncTool for GreetTool {
552    type Input = GreetInput;
553    fn metadata(&self) -> ToolInfo { ToolInfo::new(\"greet\", \"Greet\") }
554    fn call_sync(&self, input: Self::Input, _: RequestHandlerExtra)
555        -> pmcp::Result<CallToolResult> {
556        Ok(CallToolResult::text(format!(\"Hello {}\", input.name)))
557    }
558}
559```
560
561### 3. Key differences
562- Rust uses `serde` for schema generation (no zod equivalent needed)
563- Error handling uses `Result<T, pmcp::Error>` instead of thrown exceptions
564- Server building uses a builder pattern: `Server::builder().tool_typed(T).build()`
565- HTTP transport uses `StreamableHttpServer` (not SSE)";
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use pmcp::server::PromptHandler;
571
572    #[test]
573    fn all_prompts_have_metadata() {
574        let quickstart = QuickstartPrompt;
575        assert!(quickstart.metadata().is_some());
576        assert_eq!(quickstart.metadata().unwrap().name, "quickstart");
577
578        let create = CreateMcpServerPrompt;
579        assert!(create.metadata().is_some());
580        assert_eq!(create.metadata().unwrap().name, "create-mcp-server");
581
582        let add_tool = AddToolPrompt;
583        assert!(add_tool.metadata().is_some());
584        assert_eq!(add_tool.metadata().unwrap().name, "add-tool");
585
586        let diagnose = DiagnosePrompt;
587        assert!(diagnose.metadata().is_some());
588        assert_eq!(diagnose.metadata().unwrap().name, "diagnose");
589
590        let auth = SetupAuthPrompt;
591        assert!(auth.metadata().is_some());
592        assert_eq!(auth.metadata().unwrap().name, "setup-auth");
593
594        let debug = DebugProtocolErrorPrompt;
595        assert!(debug.metadata().is_some());
596        assert_eq!(debug.metadata().unwrap().name, "debug-protocol-error");
597
598        let migrate = MigratePrompt;
599        assert!(migrate.metadata().is_some());
600        assert_eq!(migrate.metadata().unwrap().name, "migrate");
601    }
602
603    #[test]
604    fn required_args_flagged_correctly() {
605        let create = CreateMcpServerPrompt;
606        let meta = create.metadata().unwrap();
607        let args = meta.arguments.unwrap();
608        assert!(args.iter().find(|a| a.name == "name").unwrap().required);
609        assert!(!args.iter().find(|a| a.name == "template").unwrap().required);
610    }
611}