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)| {
92 ResourceInfo::new(*uri, *name)
93 .with_description(*description)
94 .with_mime_type("text/markdown")
95 })
96 .collect();
97 Ok(ListResourcesResult::new(resources))
98 }
99
100 async fn read(
101 &self,
102 uri: &str,
103 _extra: RequestHandlerExtra,
104 ) -> pmcp::Result<ReadResourceResult> {
105 match content_for_uri(uri) {
106 Some(text) => Ok(ReadResourceResult::new(vec![Content::Resource {
107 uri: uri.to_string(),
108 text: Some(text.to_string()),
109 mime_type: Some("text/markdown".to_string()),
110 meta: None,
111 }])),
112 None => Err(pmcp::Error::not_found(format!(
113 "Unknown documentation resource: {uri}"
114 ))),
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn doc_resources_has_nine_entries() {
125 assert_eq!(DOC_RESOURCES.len(), 9);
126 }
127
128 #[test]
129 fn all_uris_resolve_to_content() {
130 for (uri, _, _) in DOC_RESOURCES {
131 assert!(
132 content_for_uri(uri).is_some(),
133 "URI {uri} should resolve to content"
134 );
135 }
136 }
137
138 #[test]
139 fn unknown_uri_returns_none() {
140 assert!(content_for_uri("pmcp://docs/unknown").is_none());
141 }
142}