1use regex::Regex;
2use rmcp::{
3 ErrorData as McpError, ServerHandler,
4 model::*,
5 service::{RequestContext, RoleServer},
6};
7use serde_json::json;
8use tracing::{debug, info, instrument};
9
10use super::core::NeovimMcpServer;
11
12fn new_resource(uri: &str, name: &str, description: Option<&str>) -> Resource {
13 Resource {
14 raw: RawResource {
15 uri: uri.to_string(),
16 name: name.to_string(),
17 description: description.map(|s| s.to_string()),
18 mime_type: Some("application/json".to_string()),
19 size: None,
20 icons: None,
21 title: None,
22 },
23 annotations: None,
24 }
25}
26impl ServerHandler for NeovimMcpServer {
28 #[instrument(skip(self))]
29 fn get_info(&self) -> ServerInfo {
30 ServerInfo {
31 instructions: None,
32 capabilities: ServerCapabilities::builder()
33 .enable_tools()
34 .enable_tool_list_changed()
35 .enable_resources()
36 .build(),
37 ..Default::default()
38 }
39 }
40
41 #[instrument(skip(self))]
42 async fn list_resources(
43 &self,
44 _request: Option<PaginatedRequestParam>,
45 _: RequestContext<RoleServer>,
46 ) -> Result<ListResourcesResult, McpError> {
47 debug!("Listing available diagnostic resources");
48
49 let mut resources = vec![
50 new_resource(
51 "nvim-connections://",
52 "Active Neovim Connections",
53 Some("List of active Neovim connections"),
54 ),
55 new_resource(
56 "nvim-tools://",
57 "Tool Registration Overview",
58 Some("Overview of all tools and their connection mappings"),
59 ),
60 ];
61
62 for connection_entry in self.nvim_clients.iter() {
64 let connection_id = connection_entry.key().clone();
65
66 resources.push(new_resource(
68 &format!("nvim-diagnostics://{connection_id}/workspace"),
69 &format!("Workspace Diagnostics ({connection_id})"),
70 Some(&format!(
71 "Diagnostic messages for connection {connection_id}"
72 )),
73 ));
74
75 resources.push(new_resource(
77 &format!("nvim-tools://{connection_id}"),
78 &format!("Tools for Connection ({connection_id})"),
79 Some(&format!(
80 "List of tools available for connection {connection_id}"
81 )),
82 ));
83 }
84
85 Ok(ListResourcesResult {
86 resources,
87 next_cursor: None,
88 })
89 }
90
91 #[instrument(skip(self))]
92 async fn read_resource(
93 &self,
94 ReadResourceRequestParam { uri }: ReadResourceRequestParam,
95 _: RequestContext<RoleServer>,
96 ) -> Result<ReadResourceResult, McpError> {
97 debug!("Reading resource: {}", uri);
98
99 match uri.as_str() {
100 "nvim-connections://" => {
101 let connections: Vec<_> = self
102 .nvim_clients
103 .iter()
104 .map(|entry| {
105 json!({
106 "id": entry.key(),
107 "target": entry.value().target()
108 .unwrap_or_else(|| "Unknown".to_string())
109 })
110 })
111 .collect();
112
113 Ok(ReadResourceResult {
114 contents: vec![ResourceContents::text(
115 serde_json::to_string_pretty(&connections).map_err(|e| {
116 McpError::internal_error(
117 "Failed to serialize connections",
118 Some(json!({"error": e.to_string()})),
119 )
120 })?,
121 uri,
122 )],
123 })
124 }
125 "nvim-tools://" => {
126 let static_tools: Vec<_> = self
128 .hybrid_router
129 .static_router()
130 .list_all()
131 .into_iter()
132 .map(|tool| {
133 json!({
134 "name": tool.name,
135 "description": tool.description,
136 "type": "static",
137 "available_to": "all_connections"
138 })
139 })
140 .collect();
141
142 let mut connection_tools = json!({});
143 for connection_entry in self.nvim_clients.iter() {
144 let connection_id = connection_entry.key();
145 let tools_info = self.hybrid_router.get_connection_tools_info(connection_id);
146 let dynamic_tools: Vec<_> = tools_info
147 .into_iter()
148 .filter(|(_, _, is_static)| !is_static) .map(|(name, description, _)| {
150 json!({
151 "name": name,
152 "description": description,
153 "type": "dynamic"
154 })
155 })
156 .collect();
157
158 connection_tools[connection_id] = json!(dynamic_tools);
159 }
160
161 let overview = json!({
162 "static_tools": static_tools,
163 "connection_specific_tools": connection_tools
164 });
165
166 Ok(ReadResourceResult {
167 contents: vec![ResourceContents::text(
168 serde_json::to_string_pretty(&overview).map_err(|e| {
169 McpError::internal_error(
170 "Failed to serialize tools overview",
171 Some(json!({"error": e.to_string()})),
172 )
173 })?,
174 uri,
175 )],
176 })
177 }
178 uri if uri.starts_with("nvim-tools://") => {
179 let connection_id = uri.strip_prefix("nvim-tools://").unwrap();
181
182 if connection_id.is_empty() {
183 return Err(McpError::invalid_params(
184 "Missing connection ID in tools URI",
185 None,
186 ));
187 }
188
189 let _client = self.get_connection(connection_id)?;
191
192 let tools_info_data = self.hybrid_router.get_connection_tools_info(connection_id);
194 let tools_info: Vec<_> = tools_info_data
195 .into_iter()
196 .map(|(name, description, is_static)| {
197 json!({
198 "name": name,
199 "description": description,
200 "type": if is_static { "static" } else { "dynamic" },
201 "connection_id": connection_id
202 })
203 })
204 .collect();
205
206 let result = json!({
207 "connection_id": connection_id,
208 "tools": tools_info,
209 "total_count": tools_info.len(),
210 "dynamic_count": self.hybrid_router.get_connection_tool_count(connection_id)
211 });
212
213 Ok(ReadResourceResult {
214 contents: vec![ResourceContents::text(
215 serde_json::to_string_pretty(&result).map_err(|e| {
216 McpError::internal_error(
217 "Failed to serialize connection tools",
218 Some(json!({"error": e.to_string()})),
219 )
220 })?,
221 uri,
222 )],
223 })
224 }
225 uri if uri.starts_with("nvim-diagnostics://") => {
226 let connection_diagnostics_regex = Regex::new(r"nvim-diagnostics://([^/]+)/(.+)")
228 .map_err(|e| {
229 McpError::internal_error(
230 "Failed to compile regex",
231 Some(json!({"error": e.to_string()})),
232 )
233 })?;
234
235 if let Some(captures) = connection_diagnostics_regex.captures(uri) {
236 let connection_id = captures.get(1).unwrap().as_str();
237 let resource_type = captures.get(2).unwrap().as_str();
238
239 let client = self.get_connection(connection_id)?;
240
241 match resource_type {
242 "workspace" => {
243 let diagnostics = client.get_workspace_diagnostics().await?;
244 Ok(ReadResourceResult {
245 contents: vec![ResourceContents::text(
246 serde_json::to_string_pretty(&diagnostics).map_err(|e| {
247 McpError::internal_error(
248 "Failed to serialize workspace diagnostics",
249 Some(json!({"error": e.to_string()})),
250 )
251 })?,
252 uri,
253 )],
254 })
255 }
256 path if path.starts_with("buffer/") => {
257 let buffer_id = path
258 .strip_prefix("buffer/")
259 .and_then(|s| s.parse::<u64>().ok())
260 .ok_or_else(|| {
261 McpError::invalid_params("Invalid buffer ID", None)
262 })?;
263
264 let diagnostics = client.get_buffer_diagnostics(buffer_id).await?;
265 Ok(ReadResourceResult {
266 contents: vec![ResourceContents::text(
267 serde_json::to_string_pretty(&diagnostics).map_err(|e| {
268 McpError::internal_error(
269 "Failed to serialize buffer diagnostics",
270 Some(json!({"error": e.to_string()})),
271 )
272 })?,
273 uri,
274 )],
275 })
276 }
277 _ => Err(McpError::resource_not_found(
278 "resource_not_found",
279 Some(json!({"uri": uri})),
280 )),
281 }
282 } else {
283 Err(McpError::resource_not_found(
284 "resource_not_found",
285 Some(json!({"uri": uri})),
286 ))
287 }
288 }
289 _ => Err(McpError::resource_not_found(
290 "resource_not_found",
291 Some(json!({"uri": uri})),
292 )),
293 }
294 }
295
296 #[instrument(skip(self))]
298 async fn list_tools(
299 &self,
300 _request: Option<PaginatedRequestParam>,
301 _: RequestContext<RoleServer>,
302 ) -> Result<ListToolsResult, McpError> {
303 debug!("Listing tools (static + dynamic) via HybridToolRouter");
304
305 let mut tools = self.hybrid_router.list_all_tools();
307
308 for tool in &mut tools {
309 if let Some(extra) = self.get_tool_extra_description(&tool.name) {
310 if let Some(desc) = &mut tool.description {
311 let new_desc = format!("{}\n\n{}", desc, extra).trim().to_string();
313 *desc = new_desc.into();
314 } else {
315 tool.description = Some(extra.into());
316 }
317 }
318 }
319
320 if self.nvim_clients.is_empty() {
321 info!("filter out the connection-awared tools if no connections");
322 tools.retain(|tool| {
323 !tool
324 .input_schema
325 .get("properties")
326 .map(|x| {
327 if let serde_json::Value::Object(x) = x {
328 x.contains_key("connection_id")
329 } else {
330 false
331 }
332 })
333 .unwrap_or_default()
334 });
335 }
336
337 Ok(ListToolsResult {
338 tools,
339 next_cursor: None,
340 })
341 }
342
343 #[instrument(skip(self))]
345 async fn call_tool(
346 &self,
347 CallToolRequestParam { name, arguments }: CallToolRequestParam,
348 context: RequestContext<RoleServer>,
349 ) -> Result<CallToolResult, McpError> {
350 debug!("Calling tool: {} via HybridToolRouter", name);
351
352 let args = arguments.unwrap_or_default();
354 let args_value = serde_json::to_value(args).map_err(|e| {
355 McpError::invalid_params(
356 "Failed to serialize arguments",
357 Some(json!({"error": e.to_string()})),
358 )
359 })?;
360
361 self.hybrid_router
363 .call_tool(self, &name, args_value, context)
364 .await
365 }
366}