Skip to main content

chub_cli/mcp/
server.rs

1use std::sync::Arc;
2
3use rmcp::model::{Implementation, ServerCapabilities, ServerInfo};
4use rmcp::transport::stdio;
5use rmcp::{tool_handler, ServerHandler, ServiceExt};
6
7use super::tools::ChubMcpServer;
8
9#[tool_handler]
10impl ServerHandler for ChubMcpServer {
11    fn get_info(&self) -> ServerInfo {
12        ServerInfo::new(
13            ServerCapabilities::builder()
14                .enable_tools()
15                .enable_resources()
16                .build(),
17        )
18        .with_server_info(Implementation::new("chub", env!("CARGO_PKG_VERSION")))
19        .with_instructions(
20            "Context Hub MCP Server - search and retrieve LLM-optimized docs and skills",
21        )
22    }
23
24    async fn list_resources(
25        &self,
26        _request: Option<rmcp::model::PaginatedRequestParams>,
27        _context: rmcp::service::RequestContext<rmcp::RoleServer>,
28    ) -> Result<rmcp::model::ListResourcesResult, rmcp::model::ErrorData> {
29        use rmcp::model::{Annotated, RawResource};
30
31        let resource = RawResource::new("chub://registry", "Context Hub Registry")
32            .with_mime_type("application/json")
33            .with_description("Browse the full Context Hub registry of docs and skills");
34
35        Ok(rmcp::model::ListResourcesResult::with_all_items(vec![
36            Annotated::new(resource, None),
37        ]))
38    }
39
40    async fn read_resource(
41        &self,
42        request: rmcp::model::ReadResourceRequestParams,
43        _context: rmcp::service::RequestContext<rmcp::RoleServer>,
44    ) -> Result<rmcp::model::ReadResourceResult, rmcp::model::ErrorData> {
45        use chub_core::registry::list_entries;
46        use chub_core::registry::SearchFilters;
47
48        if request.uri.as_str() != "chub://registry" {
49            return Err(rmcp::model::ErrorData::invalid_params(
50                "Resource not found",
51                None,
52            ));
53        }
54
55        let entries = list_entries(&SearchFilters::default(), &self.merged);
56        let simplified: Vec<serde_json::Value> = entries
57            .iter()
58            .map(|entry| {
59                let mut val = serde_json::json!({
60                    "id": entry.id(),
61                    "name": entry.name(),
62                    "type": entry.entry_type,
63                    "description": entry.description(),
64                    "tags": entry.tags(),
65                });
66                if let Some(languages) = entry.languages() {
67                    val["languages"] = serde_json::json!(languages
68                        .iter()
69                        .map(|l| serde_json::json!({
70                            "language": l.language,
71                            "versions": l.versions.iter().map(|v| &v.version).collect::<Vec<_>>(),
72                            "recommended": l.recommended_version,
73                        }))
74                        .collect::<Vec<_>>());
75                }
76                val
77            })
78            .collect();
79
80        let text = serde_json::to_string_pretty(&serde_json::json!({
81            "entries": simplified,
82            "total": simplified.len(),
83        }))
84        .unwrap_or_default();
85
86        Ok(rmcp::model::ReadResourceResult::new(vec![
87            rmcp::model::ResourceContents::text(request.uri, text),
88        ]))
89    }
90}
91
92/// Run the MCP stdio server.
93pub async fn run_mcp_server() -> Result<(), Box<dyn std::error::Error>> {
94    eprintln!("[chub-mcp] Starting server...");
95
96    // Best-effort registry load
97    if let Err(e) = chub_core::fetch::ensure_registry().await {
98        eprintln!("[chub-mcp] Warning: Registry not loaded: {}", e);
99    }
100
101    let merged = Arc::new(chub_core::registry::load_merged());
102    let server = ChubMcpServer::new(merged);
103
104    eprintln!("[chub-mcp] Server started (v{})", env!("CARGO_PKG_VERSION"));
105
106    let transport = stdio();
107    let running = server.serve(transport).await?;
108
109    // Wait for either: the MCP server to finish, or a shutdown signal.
110    // This prevents orphan processes when the parent MCP host terminates.
111    // When the parent closes stdin, rmcp's transport should detect EOF and
112    // cause `waiting()` to return. The ctrl_c handler catches explicit signals.
113    tokio::select! {
114        result = running.waiting() => {
115            result?;
116        }
117        _ = tokio::signal::ctrl_c() => {
118            eprintln!("[chub-mcp] received interrupt, shutting down.");
119        }
120    }
121
122    Ok(())
123}