nvim_mcp/server/
resources.rs

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}
26// Manual ServerHandler implementation to override tool methods
27impl 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        // Add connection-specific resources
63        for connection_entry in self.nvim_clients.iter() {
64            let connection_id = connection_entry.key().clone();
65
66            // Add diagnostic resource
67            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            // Add connection-specific tools resource
76            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                // Overview of all tools and their connection mappings
127                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) // Only show dynamic tools
149                        .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                // Handle connection-specific tool resources like "nvim-tools://{connection_id}"
180                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                // Verify connection exists
190                let _client = self.get_connection(connection_id)?;
191
192                // Get clean tools info for this connection
193                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                // Parse connection_id from URI pattern using regex
227                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    // Override list_tools to use HybridToolRouter
297    #[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        // Get tools from HybridToolRouter instead of static router
306        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                    // Follow the markdown format, ensuring two new lines between paragraphs
312                    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    // Override call_tool to use HybridToolRouter
344    #[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        // Convert arguments to serde_json::Value
353        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        // Use HybridToolRouter for dispatch
362        self.hybrid_router
363            .call_tool(self, &name, args_value, context)
364            .await
365    }
366}