Skip to main content

pmcp_server/resources/
docs.rs

1//! Documentation resource handler.
2//!
3//! Serves embedded SDK documentation via `pmcp://docs/*` URIs.
4//! All content is compiled into the binary -- no runtime file I/O.
5
6use crate::content::{best_practices, cli_guide, sdk_reference};
7use async_trait::async_trait;
8use pmcp::types::{Content, ListResourcesResult, ReadResourceResult, ResourceInfo};
9use pmcp::RequestHandlerExtra;
10
11/// Resource handler that serves embedded PMCP SDK documentation.
12///
13/// Routes `pmcp://docs/*` URIs to compile-time embedded markdown content.
14/// Returns 9 documentation resources covering the SDK API, CLI, and best practices.
15pub struct DocsResourceHandler;
16
17/// All available documentation resources with their URIs, names, and descriptions.
18const 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
66/// Look up the embedded content for a given documentation URI.
67fn 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}