Skip to main content

rust_doctor/mcp/
handler.rs

1use rmcp::handler::server::router::prompt::PromptRouter;
2use rmcp::handler::server::tool::ToolRouter;
3use rmcp::model::{
4    AnnotateAble, GetPromptRequestParams, GetPromptResult, ListPromptsResult, ListResourcesResult,
5    PaginatedRequestParams, RawResource, ReadResourceRequestParams, ReadResourceResult, Resource,
6    ResourceContents, ServerCapabilities, ServerInfo,
7};
8use rmcp::service::RequestContext;
9use rmcp::{ErrorData as McpError, RoleServer, prompt_handler};
10
11use super::rules::{get_rule_explanation, rule_docs};
12
13// ---------------------------------------------------------------------------
14// MCP server struct
15// ---------------------------------------------------------------------------
16
17#[derive(Clone)]
18pub struct RustDoctorServer {
19    pub(super) tool_router: ToolRouter<Self>,
20    pub(super) prompt_router: PromptRouter<Self>,
21}
22
23// ---------------------------------------------------------------------------
24// ServerHandler implementation
25// ---------------------------------------------------------------------------
26
27#[rmcp::tool_handler]
28#[prompt_handler(router = self.prompt_router)]
29impl rmcp::handler::server::ServerHandler for RustDoctorServer {
30    fn get_info(&self) -> ServerInfo {
31        ServerInfo::new(
32            ServerCapabilities::builder()
33                .enable_tools()
34                .enable_resources()
35                .enable_prompts()
36                .enable_logging()
37                .build(),
38        )
39        .with_instructions(
40            "rust-doctor is a Rust code health scanner. It analyzes projects for security, \
41             performance, correctness, architecture, and dependency issues.\n\n\
42             ## Recommended workflow\n\
43             1. `scan` a project directory → get diagnostics + score (5-30s)\n\
44             2. `explain_rule` for any rule you want to understand → instant\n\
45             3. `list_rules` to browse all available checks → instant\n\
46             4. `score` for a quick pass/fail without diagnostics (same 5-30s as scan)\n\n\
47             ## Resources\n\
48             - `rule://` — read rule documentation by URI (e.g. `rule://unwrap-in-production`)\n\n\
49             ## Prompts\n\
50             - `deep-audit` — comprehensive expert audit: explores codebase, scans, deep code review, \
51             web research for best practices, synthesis report, then offers to implement all fixes / generate PRD / manual\n\
52             - `health-check` — quick health check with scan + prioritized remediation plan + fix workflow\n\n\
53             ## Tips\n\
54             - Prefer `scan` over `score` — it includes the score plus full diagnostics\n\
55             - Use `diff` parameter in scan to focus on changed files only\n\
56             - All tools are read-only and safe to call repeatedly\n\
57             - `explain_rule` and `list_rules` are instant (no project scanning)",
58        )
59    }
60
61    async fn list_resources(
62        &self,
63        _request: Option<rmcp::model::PaginatedRequestParams>,
64        _context: RequestContext<RoleServer>,
65    ) -> Result<ListResourcesResult, McpError> {
66        let docs = rule_docs();
67        let resources: Vec<Resource> = docs
68            .iter()
69            .map(|doc| {
70                RawResource::new(format!("rule://{}", doc.name), doc.name)
71                    .with_description(format!("[{}] {}", doc.severity, doc.description))
72                    .with_mime_type("text/markdown")
73                    .no_annotation()
74            })
75            .collect();
76
77        Ok(ListResourcesResult {
78            resources,
79            next_cursor: None,
80            meta: None,
81        })
82    }
83
84    async fn read_resource(
85        &self,
86        request: ReadResourceRequestParams,
87        _context: RequestContext<RoleServer>,
88    ) -> Result<ReadResourceResult, McpError> {
89        let uri = request.uri.as_str();
90        let rule_name = uri.strip_prefix("rule://").ok_or_else(|| {
91            McpError::invalid_params(
92                format!("Unknown URI scheme: {uri}. Expected rule://{{rule-name}}"),
93                None,
94            )
95        })?;
96
97        let explanation = get_rule_explanation(rule_name);
98        Ok(ReadResourceResult::new(vec![
99            ResourceContents::text(explanation, uri).with_mime_type("text/markdown"),
100        ]))
101    }
102}