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