1use crate::content::{best_practices, cli_guide, sdk_reference};
7use async_trait::async_trait;
8use pmcp::types::{Content, ListResourcesResult, ReadResourceResult, ResourceInfo};
9use pmcp::RequestHandlerExtra;
10
11pub struct DocsResourceHandler;
16
17const DOC_RESOURCES: &[(&str, &str, &str)] = &[
19 (
20 "pmcp://docs/typed-tools",
21 "Typed Tools Guide",
22 "TypedTool, TypedSyncTool, and TypedToolWithOutput patterns",
23 ),
24 (
25 "pmcp://docs/resources",
26 "Resources Guide",
27 "ResourceHandler trait, URI patterns, and static content",
28 ),
29 (
30 "pmcp://docs/prompts",
31 "Prompts Guide",
32 "PromptHandler trait, PromptInfo metadata, and workflow prompts",
33 ),
34 (
35 "pmcp://docs/auth",
36 "Authentication Guide",
37 "OAuth, API key, and JWT middleware configuration",
38 ),
39 (
40 "pmcp://docs/middleware",
41 "Middleware Guide",
42 "Tool and protocol middleware composition",
43 ),
44 (
45 "pmcp://docs/mcp-apps",
46 "MCP Apps Guide",
47 "Widget UIs, _meta emission, and host layer integration",
48 ),
49 (
50 "pmcp://docs/error-handling",
51 "Error Handling Guide",
52 "Error variants, Result patterns, and error propagation",
53 ),
54 (
55 "pmcp://docs/cli",
56 "CLI Reference",
57 "cargo-pmcp commands: init, test, preview, deploy, and more",
58 ),
59 (
60 "pmcp://docs/best-practices",
61 "Best Practices",
62 "Tool design, resource organization, testing, and deployment",
63 ),
64];
65
66fn content_for_uri(uri: &str) -> Option<&'static str> {
68 match uri {
69 "pmcp://docs/typed-tools" => Some(sdk_reference::TYPED_TOOLS),
70 "pmcp://docs/resources" => Some(sdk_reference::RESOURCES),
71 "pmcp://docs/prompts" => Some(sdk_reference::PROMPTS),
72 "pmcp://docs/auth" => Some(sdk_reference::AUTH),
73 "pmcp://docs/middleware" => Some(sdk_reference::MIDDLEWARE),
74 "pmcp://docs/mcp-apps" => Some(sdk_reference::MCP_APPS),
75 "pmcp://docs/error-handling" => Some(sdk_reference::ERROR_HANDLING),
76 "pmcp://docs/cli" => Some(cli_guide::GUIDE),
77 "pmcp://docs/best-practices" => Some(best_practices::BEST_PRACTICES),
78 _ => None,
79 }
80}
81
82#[async_trait]
83impl pmcp::server::ResourceHandler for DocsResourceHandler {
84 async fn list(
85 &self,
86 _cursor: Option<String>,
87 _extra: RequestHandlerExtra,
88 ) -> pmcp::Result<ListResourcesResult> {
89 let resources = DOC_RESOURCES
90 .iter()
91 .map(|(uri, name, description)| ResourceInfo {
92 uri: (*uri).to_string(),
93 name: (*name).to_string(),
94 description: Some((*description).to_string()),
95 mime_type: Some("text/markdown".to_string()),
96 meta: None,
97 })
98 .collect();
99 Ok(ListResourcesResult::new(resources))
100 }
101
102 async fn read(
103 &self,
104 uri: &str,
105 _extra: RequestHandlerExtra,
106 ) -> pmcp::Result<ReadResourceResult> {
107 match content_for_uri(uri) {
108 Some(text) => Ok(ReadResourceResult::new(vec![Content::Resource {
109 uri: uri.to_string(),
110 text: Some(text.to_string()),
111 mime_type: Some("text/markdown".to_string()),
112 meta: None,
113 }])),
114 None => Err(pmcp::Error::not_found(format!(
115 "Unknown documentation resource: {uri}"
116 ))),
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn doc_resources_has_nine_entries() {
127 assert_eq!(DOC_RESOURCES.len(), 9);
128 }
129
130 #[test]
131 fn all_uris_resolve_to_content() {
132 for (uri, _, _) in DOC_RESOURCES {
133 assert!(
134 content_for_uri(uri).is_some(),
135 "URI {uri} should resolve to content"
136 );
137 }
138 }
139
140 #[test]
141 fn unknown_uri_returns_none() {
142 assert!(content_for_uri("pmcp://docs/unknown").is_none());
143 }
144}