diff --git a/CHANGELOG.md b/CHANGELOG.md index d1dc3a9..cc2eb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ All notable changes to this project will be documented in this file. ### New Features +- **Automatic Connection**: Added automatic connection feature with CLI support + for seamless integration with current project Neovim instances +- **Project-Scoped Auto-Discovery**: Automatically find and connect to Neovim + instances associated with the current project directory +- **Flexible Connection Modes**: Support for manual, automatic, and specific + target connection modes via CLI - **HTTP Server Transport**: Added HTTP server mode for web-based integrations with streamable HTTP transport support - **Multi-Transport Support**: Server now supports both stdio (default) and @@ -19,9 +25,25 @@ All notable changes to this project will be documented in this file. ### New CLI Options +- `--connect ` - Connection mode: 'manual' (default), 'auto', or specific + target (TCP address/socket path) + - `manual`: Traditional workflow using get_targets and connect tools + - `auto`: Automatically connect to all project-associated Neovim instances + - Specific target: Direct connection to TCP address or socket path - `--http-port ` - Enable HTTP server mode on the specified port - `--http-host ` - HTTP server bind address (defaults to 127.0.0.1) +### Auto-Connection Behavior + +- **Project Detection**: Automatically detects current project root using git + repository or working directory +- **Socket Pattern Matching**: Finds Neovim instances using project-specific + socket naming patterns +- **Graceful Fallback**: Continues serving with manual connection capability + if auto-connection fails +- **Connection Validation**: Validates target formats and provides clear error + messages for invalid targets + ### Dependencies - Added `hyper` for high-performance HTTP server transport diff --git a/CLAUDE.md b/CLAUDE.md index 36cf42c..9d031bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,11 +23,18 @@ with proper error handling throughout. cargo build cargo run +# Auto-connect to current project Neovim instances +cargo run -- --connect auto + +# Connect to specific target +cargo run -- --connect 127.0.0.1:6666 +cargo run -- --connect /tmp/nvim.sock + # With custom logging options cargo run -- --log-file ./nvim-mcp.log --log-level debug -# HTTP server mode -cargo run -- --http-port 8080 +# HTTP server mode with auto-connection +cargo run -- --http-port 8080 --connect auto # HTTP server mode with custom bind address cargo run -- --http-port 8080 --http-host 0.0.0.0 @@ -42,6 +49,10 @@ nix develop . **CLI Options:** +- `--connect `: Connection mode (defaults to manual) + - `manual`: Traditional workflow using get_targets and connect tools + - `auto`: Automatically connect to all project-associated Neovim instances + - Specific target: Direct connection to TCP address or socket path - `--log-file `: Log file path (defaults to stderr) - `--log-level `: Log level (trace, debug, info, warn, error; defaults to info) diff --git a/README.md b/README.md index a1b42c5..2541ac2 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,21 @@ cargo install --path . ### 1. Start the Server ```bash -# Start as stdio MCP server (default) +# Start as stdio MCP server (default, manual connection mode) nvim-mcp +# Auto-connect to current project Neovim instances +nvim-mcp --connect auto + +# Connect to specific target (TCP address or socket path) +nvim-mcp --connect 127.0.0.1:6666 +nvim-mcp --connect /tmp/nvim.sock + # With custom logging nvim-mcp --log-file ./nvim-mcp.log --log-level debug -# HTTP server mode -nvim-mcp --http-port 8080 +# HTTP server mode with auto-connection +nvim-mcp --http-port 8080 --connect auto # HTTP server mode with custom bind address nvim-mcp --http-port 8080 --http-host 0.0.0.0 @@ -64,6 +71,10 @@ nvim-mcp --http-port 8080 --http-host 0.0.0.0 #### Command Line Options +- `--connect `: Connection mode (default: manual) + - `manual`: Traditional workflow using get_targets and connect tools + - `auto`: Automatically connect to all project-associated Neovim instances + - Specific target: TCP address (e.g., `127.0.0.1:6666`) or absolute socket path - `--log-file `: Path to log file (defaults to stderr) - `--log-level `: Log level (trace, debug, info, warn, error; defaults to info) @@ -106,11 +117,51 @@ Or add to your Neovim config: vim.fn.serverstart("127.0.0.1:6666") ``` -### 3. Basic Usage Workflow +### 3. Usage Workflows + +Once both the MCP server and Neovim are running, here are the available workflows: + +#### Automatic Connection Mode (Recommended) + +When using `--connect auto`, the server automatically discovers and connects to +Neovim instances associated with your current project: + +1. **Start server with auto-connect**: + + ```bash + nvim-mcp --connect auto + ``` + +2. **Server automatically**: + - Detects current project root (git repository or working directory) + - Finds all Neovim instances for the current project + - Establishes connections with deterministic `connection_id`s + - Reports connection status and IDs +3. **Use connection-aware tools directly**: + - Server logs will show the `connection_id`s for connected instances + - Use tools like `list_buffers`, `buffer_diagnostics`, etc. with these IDs + - Access resources immediately without manual connection setup -Once both the MCP server and Neovim are running, here's a typical workflow: +#### Specific Target Mode -#### Using Unix Socket (Recommended) +For direct connection to a known target: + +1. **Connect to specific target**: + + ```bash + # TCP connection + nvim-mcp --connect 127.0.0.1:6666 + + # Unix socket connection + nvim-mcp --connect /tmp/nvim.sock + ``` + +2. **Server automatically connects and reports the `connection_id`** +3. **Use connection-aware tools with the reported ID** + +#### Manual Connection Mode (Traditional) + +For traditional discovery-based workflow: 1. **Discover available Neovim instances**: - Use `get_targets` tool to list available socket paths @@ -125,13 +176,6 @@ Once both the MCP server and Neovim are running, here's a typical workflow: 4. **Optional cleanup**: - Use `disconnect` tool when completely done -#### Using TCP Connection - -1. **Connect to TCP endpoint**: - - Use `connect_tcp` tool with address like "127.0.0.1:6666" - - Save the returned `connection_id` -2. **Follow steps 3-4 above** with your connection ID - ## HTTP Server Transport The server supports HTTP transport mode for web-based integrations and diff --git a/docs/instructions.md b/docs/instructions.md index d661f26..6adce2c 100644 --- a/docs/instructions.md +++ b/docs/instructions.md @@ -311,6 +311,20 @@ Connection-scoped diagnostic resources using `nvim-diagnostics://` scheme: ### Connection Workflow for LLMs +#### Automatic Connection (Recommended) + +When the nvim-mcp server is started with `--connect auto`, connections are +established automatically: + +1. **Pre-established Connections**: Server automatically discovers and connects + to project-associated Neovim instances +2. **Connection ID Retrieval**: Use the `nvim-connections://` resource to get + available `connection_id`s +3. **Direct Tool Usage**: Use connection-aware tools immediately with the + retrieved `connection_id`s + +#### Manual Connection Workflow + 1. **Discovery Phase**: Use `get_targets` to find available Neovim instances 2. **Connection Phase**: Use `connect` with a target from the discovery results 3. **Caching Phase**: Store the `connection_id` for reuse across multiple operations @@ -378,9 +392,16 @@ Connection-scoped diagnostic resources using `nvim-diagnostics://` scheme: ### Integration Workflows +#### Automatic Connection Workflow + +1. Start server with `nvim-mcp --connect auto` +2. Read nvim-connections:// resource to get available connection IDs +3. Use connection IDs directly with connection-aware tools +4. Server maintains all connections automatically + #### Diagnostic Analysis -1. Connect to Neovim instance (cache connection_id) +1. Connect to Neovim instance (cache connection_id) or use auto-connected IDs 2. Read workspace diagnostics resource 3. Group diagnostics by severity and file 4. Use buffer_diagnostics for detailed file analysis (reuse connection_id) diff --git a/src/lib.rs b/src/lib.rs index d6edf34..86e553b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,10 @@ mod server; #[cfg(test)] pub mod test_utils; -pub use server::NeovimMcpServer; +pub use server::{ + NeovimMcpServer, + core::{auto_connect_current_project_targets, auto_connect_single_target}, +}; pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs index 3742f0a..2b0dd41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::OnceLock}; +use std::{path::PathBuf, str::FromStr, sync::OnceLock}; use clap::Parser; use hyper_util::{ @@ -13,10 +13,10 @@ use rmcp::{ streamable_http_server::session::local::LocalSessionManager, }, }; -use tracing::{error, info}; +use tracing::{error, info, warn}; use tracing_subscriber::EnvFilter; -use nvim_mcp::NeovimMcpServer; +use nvim_mcp::{NeovimMcpServer, auto_connect_current_project_targets, auto_connect_single_target}; static LONG_VERSION: OnceLock = OnceLock::new(); @@ -40,6 +40,43 @@ fn long_version() -> &'static str { .as_str() } +#[derive(Clone, Debug)] +enum ConnectBehavior { + Manual, + Auto, + SpecificTarget(String), +} + +impl FromStr for ConnectBehavior { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "manual" => Ok(ConnectBehavior::Manual), + "auto" => Ok(ConnectBehavior::Auto), + target => { + // Validate TCP address format + if target.parse::().is_ok() { + return Ok(ConnectBehavior::SpecificTarget(target.to_string())); + } + + // Validate file path (socket/pipe) + let path = std::path::Path::new(target); + if path.is_absolute() + && (path.exists() || path.parent().is_some_and(|p| p.exists())) + { + return Ok(ConnectBehavior::SpecificTarget(target.to_string())); + } + + Err(format!( + "Invalid target: '{}'. Must be 'manual', 'auto', TCP address (e.g., '127.0.0.1:6666'), or absolute socket path", + target + )) + } + } + } +} + #[derive(Parser)] #[command(version, long_version=long_version(), about, long_about = None)] struct Cli { @@ -58,6 +95,10 @@ struct Cli { /// HTTP server bind address (default: 127.0.0.1) #[arg(long, default_value = "127.0.0.1")] http_host: String, + + /// Connection mode: 'manual', 'auto', or specific target (TCP address/socket path) + #[arg(long, default_value = "manual")] + connect: ConnectBehavior, } #[tokio::main] @@ -102,6 +143,37 @@ async fn main() -> Result<(), Box> { info!("Starting nvim-mcp Neovim server"); let server = NeovimMcpServer::new(); + // Handle connection mode + match cli.connect { + ConnectBehavior::Auto => { + match auto_connect_current_project_targets(&server).await { + Ok(connections) => { + if connections.is_empty() { + info!("No Neovim instances found for current project"); + } else { + info!("Auto-connected to {} project instances", connections.len()); + } + } + Err(failures) => { + warn!("Auto-connection failed for all {} targets", failures.len()); + for (target, error) in &failures { + warn!(" {target}: {error}"); + } + // Continue serving - manual connections still possible + } + } + } + ConnectBehavior::SpecificTarget(target) => { + match auto_connect_single_target(&server, &target).await { + Ok(id) => info!("Connected to specific target {} with ID {}", target, id), + Err(e) => return Err(format!("Failed to connect to {}: {}", target, e).into()), + } + } + ConnectBehavior::Manual => { + info!("Manual connection mode - use get_targets and connect tools"); + } + } + if let Some(port) = cli.http_port { // HTTP server mode let addr = format!("{}:{}", cli.http_host, port); @@ -134,6 +206,7 @@ async fn main() -> Result<(), Box> { } } else { // Default stdio mode + info!("Starting Neovim server on stdio"); let service = server.serve(stdio()).await.inspect_err(|e| { error!("Error starting Neovim server: {}", e); })?; diff --git a/src/server/core.rs b/src/server/core.rs index 8f8317d..e9e2770 100644 --- a/src/server/core.rs +++ b/src/server/core.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use dashmap::DashMap; use rmcp::{ErrorData as McpError, handler::server::router::tool::ToolRouter}; -use tracing::debug; +use tracing::{debug, info, warn}; use crate::neovim::{NeovimClientTrait, NeovimError}; @@ -92,13 +92,6 @@ fn b3sum(input: &str) -> String { blake3::hash(input.as_bytes()).to_hex().to_string() } -/// Escape path for use in filename by replacing problematic characters -#[allow(dead_code)] -fn escape_path(path: &str) -> String { - // Remove leading/trailing whitespace and replace '/' with '%' - path.trim().replace("/", "%") -} - /// Get git root directory #[allow(dead_code)] fn get_git_root() -> Option { @@ -138,3 +131,125 @@ pub fn find_get_all_targets() -> Vec { Err(_) => Vec::new(), } } + +/// Get current project root directory +/// Tries git root first, falls back to current working directory +fn get_current_project_root() -> String { + // Try git root first + if let Some(git_root) = get_git_root() { + return git_root; + } + + // Fallback to current working directory + std::env::current_dir() + .unwrap_or_else(|err| { + warn!("Failed to get current working directory: {}", err); + std::path::PathBuf::from("") + }) + .to_string_lossy() + .to_string() +} + +/// Escape path for use in filename by replacing problematic characters +/// Matches the Lua plugin behavior: replaces '/' with '%' +fn escape_path(path: &str) -> String { + path.trim().replace("/", "%") +} + +/// Find nvim-mcp socket targets for the current project only +/// Returns sockets that match the current project's escaped path +pub fn find_targets_for_current_project() -> Vec { + let current_project_root = get_current_project_root(); + let escaped_project_root = escape_path(¤t_project_root); + + let temp_dir = get_temp_dir(); + let pattern = format!("{temp_dir}/nvim-mcp.{escaped_project_root}.*.sock"); + + match glob::glob(&pattern) { + Ok(paths) => paths + .filter_map(|entry| entry.ok()) + .map(|path| path.to_string_lossy().to_string()) + .collect(), + Err(e) => { + warn!( + "Glob error while searching for Neovim sockets with pattern '{}': {}", + pattern, e + ); + Vec::new() + } + } +} + +/// Connect to a single target and return the connection ID +/// Reusable for both auto-connect and specific target modes +pub async fn auto_connect_single_target( + server: &NeovimMcpServer, + target: &str, +) -> Result { + let connection_id = server.generate_shorter_connection_id(target); + + // Check if already connected (connection replacement logic) + if let Some(mut old_client) = server.nvim_clients.get_mut(&connection_id) { + if let Some(existing_target) = old_client.target() + && existing_target == target + { + debug!("Already connected to {target} with ID {connection_id}"); + return Ok(connection_id); // Already connected to same target + } + // Different target, disconnect old one + debug!("Disconnecting old connection for {target}"); + let _ = old_client.disconnect().await; + } + + // Import NeovimClient here to avoid circular imports + let mut client = crate::neovim::NeovimClient::new(); + client.connect_path(target).await?; + client.setup_diagnostics_changed_autocmd().await?; + + server + .nvim_clients + .insert(connection_id.clone(), Box::new(client)); + debug!("Successfully connected to {target} with ID {connection_id}"); + Ok(connection_id) +} + +/// Auto-connect to all Neovim targets for the current project +/// Returns list of successful connection IDs, or list of failures +pub async fn auto_connect_current_project_targets( + server: &NeovimMcpServer, +) -> Result, Vec<(String, String)>> { + let project_targets = find_targets_for_current_project(); + let current_project = get_current_project_root(); + + if project_targets.is_empty() { + info!("No Neovim instances found for current project: {current_project}"); + return Ok(Vec::new()); + } + + info!( + "Found {} Neovim instances for current project: {current_project}", + project_targets.len() + ); + + let mut successful_connections = Vec::new(); + let mut failed_connections = Vec::new(); + + for target in project_targets { + match auto_connect_single_target(server, &target).await { + Ok(connection_id) => { + successful_connections.push(connection_id); + info!("Auto-connected to project Neovim instance: {target}"); + } + Err(e) => { + failed_connections.push((target.clone(), e.to_string())); + warn!("Failed to auto-connect to {target}: {e}"); + } + } + } + + if successful_connections.is_empty() && !failed_connections.is_empty() { + Err(failed_connections) + } else { + Ok(successful_connections) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 119178f..02e5457 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,4 +1,4 @@ -pub(crate) mod core; +pub mod core; mod resources; pub(crate) mod tools;