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
92pub async fn run_mcp_server() -> Result<(), Box<dyn std::error::Error>> {
94 eprintln!("[chub-mcp] Starting server...");
95
96 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 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}