1use async_trait::async_trait;
8use pmcp::types::{Content, GetPromptResult, PromptArgument, PromptInfo, PromptMessage, Role};
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 {
20 role: Role::Assistant,
21 content: Content::Text { text },
22 }],
23 Some(description.to_string()),
24 ))
25}
26
27fn 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
38pub 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
120pub 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
182pub 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
252pub 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
309pub 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
429pub 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
484pub 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}