1use async_trait::async_trait;
8use pmcp::types::{Content, GetPromptResult, PromptArgument, PromptInfo, PromptMessage};
9use pmcp::RequestHandlerExtra;
10use std::collections::HashMap;
11
12fn 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
24fn 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
34pub 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
113pub 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
175pub 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
245pub 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
302pub 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
422pub 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
477pub 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}