From 1fd1d119e6ddb9996dd1982004ea93f614e2c53d Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Sat, 12 Sep 2026 08:21:41 +0100 Subject: [PATCH] fix(proxy): fully non-blocking mcp-memory-stub proxy This makes the stub purely non-blocking via tokio::spawn and fixes cross-OS compilation boundaries in build.cmd --- build.cmd | 6 +- linux-nvim/src/main.rs | 595 +++++++++++++++++++--------------------- linux-nvim/src/mcp.rs | 24 +- stub/src/main.rs | 53 ++-- win-nvim/src/main.rs | 596 +++++++++++++++++++---------------------- win-nvim/src/mcp.rs | 24 +- 6 files changed, 617 insertions(+), 681 deletions(-) diff --git a/build.cmd b/build.cmd index 1e751ca..6d30aec 100644 --- a/build.cmd +++ b/build.cmd @@ -2,14 +2,14 @@ setlocal echo Building mcp-memory (server and stub) for Windows... -rustup run stable cargo build --release +rustup run stable cargo build --release --workspace --exclude mcp-memory-linux-nvim if %ERRORLEVEL% NEQ 0 ( echo Windows build failed! exit /b %ERRORLEVEL% ) echo Building mcp-memory (server and stub) for Linux (WSL)... -rustup run stable cargo zigbuild --release --target x86_64-unknown-linux-musl +rustup run stable cargo zigbuild --release --workspace --exclude mcp-memory-win-nvim --target x86_64-unknown-linux-musl if %ERRORLEVEL% NEQ 0 ( echo Linux build failed! exit /b %ERRORLEVEL% @@ -31,7 +31,7 @@ echo. echo Copying Windows executables... copy /Y target\release\mcp-memory-server.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe" copy /Y target\release\mcp-memory-stub.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-stub.exe" -copy /Y target\x86_64-pc-windows-gnu\release\mcp-memory-win-nvim.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-win-nvim.exe" +copy /Y target\release\mcp-memory-win-nvim.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-win-nvim.exe" echo Copying Linux executables to WSL... wsl -d Ubuntu -e bash -c "cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/target/x86_64-unknown-linux-musl/release/mcp-memory-server /home/riz/.local/bin/mcp-memory-server && chmod +x /home/riz/.local/bin/mcp-memory-server" diff --git a/linux-nvim/src/main.rs b/linux-nvim/src/main.rs index 1ecd3a6..aeb7ae5 100644 --- a/linux-nvim/src/main.rs +++ b/linux-nvim/src/main.rs @@ -8,245 +8,222 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[tokio::main] async fn main() { loop { - let msg = match read_message() { + let msg = match read_message().await { Some(m) => m, None => break, }; - let id = msg.id.unwrap_or(json!(null)); + tokio::spawn(async move { + let id = msg.id.clone().unwrap_or(json!(null)); - match msg.method.as_str() { - "initialize" => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "capabilities": {}, - "serverInfo": { - "name": "mcp-memory-linux-nvim", - "version": "0.1.0" - } - })), - error: None, - }); - } - "tools/list" => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "tools": [ - { - "name": "nvim_goto_line", - "description": "Open a file and jump to a specific line", - "inputSchema": { - "type": "object", - "properties": { - "file": { "type": "string" }, - "line": { "type": "integer" } - }, - "required": ["file", "line"] - } - }, - { - "name": "nvim_get_active_buffer", - "description": "Get the contents of the currently active Neovim buffer", - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "nvim_get_cursor", - "description": "Get the current cursor position (line and column) in the active Neovim buffer", - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "nvim_get_visual_selection", - "description": "Get the text that is currently highlighted or was last highlighted in Visual mode", - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "nvim_set_diagnostics", - "description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer", - "inputSchema": { - "type": "object", - "properties": { - "line": { "type": "integer" }, - "message": { "type": "string" } - }, - "required": ["line", "message"] - } + match msg.method.as_str() { + "initialize" => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "capabilities": {}, + "serverInfo": { + "name": "mcp-memory-linux-nvim", + "version": "0.1.0" } - ] - })), - error: None, - }); - } - "tools/call" => { - let params = msg.params.unwrap_or(json!({})); - let name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); - let args = params.get("arguments").cloned().unwrap_or(json!({})); + })), + error: None, + }).await; + } + "tools/list" => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "tools": [ + { + "name": "nvim_goto_line", + "description": "Open a file and jump to a specific line", + "inputSchema": { + "type": "object", + "properties": { + "file": { "type": "string" }, + "line": { "type": "integer" } + }, + "required": ["file", "line"] + } + }, + { + "name": "nvim_get_active_buffer", + "description": "Get the contents of the currently active Neovim buffer", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "nvim_get_cursor", + "description": "Get the current cursor position (line and column) in the active Neovim buffer", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "nvim_get_visual_selection", + "description": "Get the text that is currently highlighted or was last highlighted in Visual mode", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "nvim_set_diagnostics", + "description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer", + "inputSchema": { + "type": "object", + "properties": { + "line": { "type": "integer" }, + "message": { "type": "string" } + }, + "required": ["line", "message"] + } + } + ] + })), + error: None, + }).await; + } + "tools/call" => { + let params = msg.params.clone().unwrap_or(json!({})); + let name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); + let args = params.get("arguments").cloned().unwrap_or(json!({})); - match name { - "nvim_goto_line" => { - let file = args.get("file").and_then(|v| v.as_str()).unwrap_or(""); - let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); - - match send_nvim_command(&format!("edit +{} {}", line, file)).await { - Ok(_) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + match name { + "nvim_goto_line" => { + let file = args.get("file").and_then(|v| v.as_str()).unwrap_or(""); + let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); + + let cmd = format!("edit {} | {} | normal! zz", file, line); + match send_nvim_command(&cmd).await { + Ok(_) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": format!("Successfully jumped to {}:{}", file, line) } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to execute command: {}", e)).await; + } } } - } - "nvim_get_active_buffer" => { - match get_nvim_active_buffer().await { - Ok(content) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": content}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_get_active_buffer" => { + match get_nvim_active_buffer().await { + Ok(content) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": content } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to get active buffer: {}", e)).await; + } } } - } - "nvim_get_cursor" => { - match get_nvim_cursor().await { - Ok(content) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": content}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_get_cursor" => { + match get_nvim_cursor().await { + Ok(content) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": content } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to get cursor: {}", e)).await; + } } } - } - "nvim_get_visual_selection" => { - match get_nvim_visual_selection().await { - Ok(content) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": content}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_get_visual_selection" => { + match get_nvim_visual_selection().await { + Ok(content) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": content } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to get visual selection: {}", e)).await; + } } } - } - "nvim_set_diagnostics" => { - let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); - let message = args.get("message").and_then(|v| v.as_str()).unwrap_or(""); - - match set_nvim_diagnostics(line, message).await { - Ok(_) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_set_diagnostics" => { + let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); + let message = args.get("message").and_then(|v| v.as_str()).unwrap_or(""); + + match set_nvim_diagnostics(line, message).await { + Ok(_) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": format!("Successfully set diagnostic on line {}", line) } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to set diagnostic: {}", e)).await; + } } } - } - _ => { - send_error(id, -32601, "Tool not found"); + _ => { + send_error(id, -32601, "Tool not found").await; + } } } + _ => { + // Ignore other methods + } } - _ => { - // Ignore other methods - } - } + }); } } async fn get_socket_path() -> Result { - if let Ok(content) = std::fs::read_to_string(format!("{}/.gemini/active_nvim.txt", std::env::var("HOME").unwrap_or("/home/riz".into()))) { - let p = content.trim().to_string(); - if !p.is_empty() { - return Ok(p); + // 1. Try active_nvim.txt first + if let Ok(home) = std::env::var("HOME") { + let path = format!("{}/.gemini/active_nvim.txt", home); + if let Ok(content) = std::fs::read_to_string(&path) { + let p = content.trim().to_string(); + if !p.is_empty() { + return Ok(p); + } } } - // Fallback: search /tmp/nvim.*/0 + // 2. Fallback: search /tmp/nvim.*/0 if let Ok(entries) = std::fs::read_dir("/tmp") { for entry in entries.flatten() { if let Ok(name) = entry.file_name().into_string() { @@ -262,33 +239,70 @@ async fn get_socket_path() -> Result { Err("Could not find Neovim socket".to_string()) } -async fn send_nvim_command(cmd: &str) -> Result<(), String> { +async fn call_nvim(req: rmpv::Value) -> Result { + let msgid = if let rmpv::Value::Array(ref arr) = req { + if arr.len() > 1 { arr[1].clone() } else { rmpv::Value::Nil } + } else { rmpv::Value::Nil }; + let socket_path = get_socket_path().await?; let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?; - use rmpv::Value as RmpValue; - let req = RmpValue::Array(vec![ - RmpValue::Integer(0.into()), - RmpValue::Integer(1.into()), - RmpValue::String("nvim_command".into()), - RmpValue::Array(vec![RmpValue::String(cmd.into())]), - ]); - let mut buf = Vec::new(); rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; stream.write_all(&buf).await.map_err(|e| e.to_string())?; - Ok(()) + let mut resp_buf = Vec::new(); + let mut chunk = vec![0u8; 8192]; + let mut offset = 0; + + loop { + let mut cursor = std::io::Cursor::new(&resp_buf[offset..]); + match rmpv::decode::read_value(&mut cursor) { + Ok(val) => { + offset += cursor.position() as usize; + + if let rmpv::Value::Array(ref arr) = val { + if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid { + return Ok(val); + } + } + continue; + }, + Err(_) => { + let n = stream.read(&mut chunk).await.map_err(|e| e.to_string())?; + if n == 0 { + return Err("Unexpected EOF from Neovim".to_string()); + } + resp_buf.extend_from_slice(&chunk[..n]); + } + } + } } -async fn get_nvim_active_buffer() -> Result { - let socket_path = get_socket_path().await?; - let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?; - +async fn send_nvim_command(cmd: &str) -> Result<(), String> { use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(2.into()), + RmpValue::Integer(1.into()), // msgid + RmpValue::String("nvim_command".into()), + RmpValue::Array(vec![RmpValue::String(cmd.into())]), + ]); + + let resp = call_nvim(req).await?; + if let RmpValue::Array(arr) = resp { + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + return Ok(()); + } + Err("Invalid response".to_string()) +} + +async fn get_nvim_active_buffer() -> Result { + use rmpv::Value as RmpValue; + let req = RmpValue::Array(vec![ + RmpValue::Integer(0.into()), + RmpValue::Integer(2.into()), // msgid RmpValue::String("nvim_buf_get_lines".into()), RmpValue::Array(vec![ RmpValue::Integer(0.into()), @@ -298,81 +312,55 @@ async fn get_nvim_active_buffer() -> Result { ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - stream.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 1024 * 1024]; - let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - if let RmpValue::Array(lines) = &arr[3] { - let mut text = String::new(); - for line in lines { - if let RmpValue::String(s) = line { - if let Some(s) = s.as_str() { - text.push_str(s); - text.push('\n'); - } + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + if let RmpValue::Array(lines) = &arr[3] { + let mut text = String::new(); + for line in lines { + if let RmpValue::String(s) = line { + if let Some(s) = s.as_str() { + text.push_str(s); + text.push('\n'); } } - return Ok(text); } + return Ok(text); } } - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } async fn get_nvim_cursor() -> Result { - let socket_path = get_socket_path().await?; - let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?; - use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(2.into()), + RmpValue::Integer(3.into()), // msgid RmpValue::String("nvim_win_get_cursor".into()), RmpValue::Array(vec![ RmpValue::Integer(0.into()), ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - stream.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 8192]; - let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - if let RmpValue::Array(pos) = &arr[3] { - if pos.len() == 2 { - if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) { - return Ok(format!("Line: {}, Column: {}", row, col)); - } + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + if let RmpValue::Array(pos) = &arr[3] { + if pos.len() == 2 { + if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) { + return Ok(format!("Line: {}, Column: {}", row, col)); } } } } - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } async fn get_nvim_visual_selection() -> Result { - let socket_path = get_socket_path().await?; - let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?; - let lua_script = r#" local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) @@ -387,7 +375,7 @@ async fn get_nvim_visual_selection() -> Result { use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(3.into()), + RmpValue::Integer(4.into()), // msgid RmpValue::String("nvim_exec_lua".into()), RmpValue::Array(vec![ RmpValue::String(lua_script.into()), @@ -395,37 +383,22 @@ async fn get_nvim_visual_selection() -> Result { ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - stream.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 1024 * 1024]; - let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - if let RmpValue::String(s) = &arr[3] { - if let Some(text) = s.as_str() { - return Ok(text.to_string()); - } + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + if let RmpValue::String(s) = &arr[3] { + if let Some(text) = s.as_str() { + return Ok(text.to_string()); } } } - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> { - let socket_path = get_socket_path().await?; - let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?; - - // We escape backslashes and double quotes in the message let escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\""); - let lua_script = format!(r#" local ns = vim.api.nvim_create_namespace("gemini_diagnostics") local diagnostics = {{{{ @@ -440,7 +413,7 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> { use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(4.into()), + RmpValue::Integer(5.into()), // msgid RmpValue::String("nvim_exec_lua".into()), RmpValue::Array(vec![ RmpValue::String(lua_script.into()), @@ -448,22 +421,12 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> { ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - stream.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 8192]; - let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - return Ok(()); + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); } + return Ok(()); } - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } diff --git a/linux-nvim/src/mcp.rs b/linux-nvim/src/mcp.rs index a837d57..51c7421 100644 --- a/linux-nvim/src/mcp.rs +++ b/linux-nvim/src/mcp.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct JsonRpcRequest { @@ -19,14 +20,12 @@ pub struct JsonRpcResponse { pub error: Option, } -pub fn read_message() -> Option { - use std::io::{BufRead, Read}; - let stdin = std::io::stdin(); - let mut handle = stdin.lock(); +pub async fn read_message() -> Option { + let mut stdin = BufReader::new(tokio::io::stdin()); let mut length = 0; loop { let mut line = String::new(); - if handle.read_line(&mut line).unwrap_or(0) == 0 { + if stdin.read_line(&mut line).await.unwrap_or(0) == 0 { return None; } let line = line.trim_end(); @@ -41,22 +40,23 @@ pub fn read_message() -> Option { return None; } let mut buffer = vec![0; length]; - handle.read_exact(&mut buffer).unwrap_or(()); + stdin.read_exact(&mut buffer).await.unwrap_or(0); serde_json::from_slice(&buffer).ok() } -pub fn send_response(response: JsonRpcResponse) { +pub async fn send_response(response: JsonRpcResponse) { let msg = serde_json::to_string(&response).unwrap(); - print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg); - use std::io::Write; - std::io::stdout().flush().unwrap(); + let payload = format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg); + let mut stdout = tokio::io::stdout(); + let _ = stdout.write_all(payload.as_bytes()).await; + let _ = stdout.flush().await; } -pub fn send_error(id: Value, code: i32, message: &str) { +pub async fn send_error(id: Value, code: i32, message: &str) { send_response(JsonRpcResponse { jsonrpc: "2.0".to_string(), id, result: None, error: Some(serde_json::json!({"code": code, "message": message})), - }); + }).await; } diff --git a/stub/src/main.rs b/stub/src/main.rs index f10ab6f..a7e1bc9 100644 --- a/stub/src/main.rs +++ b/stub/src/main.rs @@ -24,48 +24,51 @@ fn main() -> Result<(), Box> { let (msg_tx, mut msg_rx) = mpsc::channel::(100); let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); - tokio::task::spawn_blocking(move || { - let stdin = std::io::stdin(); - let mut handle = stdin.lock(); + tokio::spawn(async move { + let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()); let mut buffer = String::new(); - while let Ok(bytes) = std::io::BufRead::read_line(&mut handle, &mut buffer) { + while let Ok(bytes) = stdin.read_line(&mut buffer).await { if bytes == 0 { break; } - let _ = msg_tx.blocking_send(buffer.clone()); + let _ = msg_tx.send(buffer.clone()).await; buffer.clear(); } - let _ = shutdown_tx.blocking_send(()); + let _ = shutdown_tx.send(()).await; }); let target_url = cli.target; let post_url = Arc::new(RwLock::new(String::new())); - let post_url_clone = Arc::clone(&post_url); + let post_url_proxy = Arc::clone(&post_url); let client = reqwest::Client::builder().build()?; tokio::spawn(async move { while let Some(msg) = msg_rx.recv().await { - let mut attempts = 0; - loop { - let url = post_url_clone.read().await.clone(); - if !url.is_empty() { - let res = client - .post(&url) - .header("Accept", "application/json, text/event-stream") - .header("Content-Type", "application/json") - .body(msg.clone()) - .send() - .await; + let post_url_clone = Arc::clone(&post_url_proxy); + let client = client.clone(); + tokio::spawn(async move { + let mut attempts = 0; + loop { + let url = post_url_clone.read().await.clone(); + if !url.is_empty() { + let res = client + .post(&url) + .header("Accept", "application/json, text/event-stream") + .header("Content-Type", "application/json") + .body(msg.clone()) + .send() + .await; - if let Ok(resp) = res { if resp.status().is_success() { break; } } + if let Ok(resp) = res { if resp.status().is_success() { break; } } + } + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + attempts += 1; + if attempts % 10 == 0 { + eprintln!("[PROXY] Waiting for server to accept messages..."); + } } - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - attempts += 1; - if attempts % 10 == 0 { - eprintln!("[PROXY] Waiting for server to accept messages..."); - } - } + }); } }); diff --git a/win-nvim/src/main.rs b/win-nvim/src/main.rs index f537a3f..7230fac 100644 --- a/win-nvim/src/main.rs +++ b/win-nvim/src/main.rs @@ -8,237 +8,222 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[tokio::main] async fn main() { loop { - let msg = match read_message() { + let msg = match read_message().await { Some(m) => m, None => break, }; - let id = msg.id.unwrap_or(json!(null)); + tokio::spawn(async move { + let id = msg.id.clone().unwrap_or(json!(null)); - match msg.method.as_str() { - "initialize" => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "capabilities": {}, - "serverInfo": { - "name": "mcp-memory-win-nvim", - "version": "0.1.0" - } - })), - error: None, - }); - } - "tools/list" => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "tools": [ - { - "name": "nvim_goto_line", - "description": "Open a file and jump to a specific line", - "inputSchema": { - "type": "object", - "properties": { - "file": { "type": "string" }, - "line": { "type": "integer" } - }, - "required": ["file", "line"] - } - }, - { - "name": "nvim_get_active_buffer", - "description": "Get the contents of the currently active Neovim buffer", - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "nvim_get_cursor", - "description": "Get the current cursor position (line and column) in the active Neovim buffer", - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "nvim_get_visual_selection", - "description": "Get the text that is currently highlighted or was last highlighted in Visual mode", - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "nvim_set_diagnostics", - "description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer", - "inputSchema": { - "type": "object", - "properties": { - "line": { "type": "integer" }, - "message": { "type": "string" } - }, - "required": ["line", "message"] - } + match msg.method.as_str() { + "initialize" => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "capabilities": {}, + "serverInfo": { + "name": "mcp-memory-win-nvim", + "version": "0.1.0" } - ] - })), - error: None, - }); - } - "tools/call" => { - let params = msg.params.unwrap_or(json!({})); - let name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); - let args = params.get("arguments").cloned().unwrap_or(json!({})); + })), + error: None, + }).await; + } + "tools/list" => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "tools": [ + { + "name": "nvim_goto_line", + "description": "Open a file and jump to a specific line", + "inputSchema": { + "type": "object", + "properties": { + "file": { "type": "string" }, + "line": { "type": "integer" } + }, + "required": ["file", "line"] + } + }, + { + "name": "nvim_get_active_buffer", + "description": "Get the contents of the currently active Neovim buffer", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "nvim_get_cursor", + "description": "Get the current cursor position (line and column) in the active Neovim buffer", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "nvim_get_visual_selection", + "description": "Get the text that is currently highlighted or was last highlighted in Visual mode", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "nvim_set_diagnostics", + "description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer", + "inputSchema": { + "type": "object", + "properties": { + "line": { "type": "integer" }, + "message": { "type": "string" } + }, + "required": ["line", "message"] + } + } + ] + })), + error: None, + }).await; + } + "tools/call" => { + let params = msg.params.clone().unwrap_or(json!({})); + let name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); + let args = params.get("arguments").cloned().unwrap_or(json!({})); - match name { - "nvim_goto_line" => { - let file = args.get("file").and_then(|v| v.as_str()).unwrap_or(""); - let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); - - match send_nvim_command(&format!("edit +{} {}", line, file)).await { - Ok(_) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + match name { + "nvim_goto_line" => { + let file = args.get("file").and_then(|v| v.as_str()).unwrap_or(""); + let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); + + let cmd = format!("edit {} | {} | normal! zz", file, line); + match send_nvim_command(&cmd).await { + Ok(_) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": format!("Successfully jumped to {}:{}", file, line) } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to execute command: {}", e)).await; + } } } - } - "nvim_get_active_buffer" => { - match get_nvim_active_buffer().await { - Ok(content) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": content}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_get_active_buffer" => { + match get_nvim_active_buffer().await { + Ok(content) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": content } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to get active buffer: {}", e)).await; + } } } - } - "nvim_get_cursor" => { - match get_nvim_cursor().await { - Ok(content) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": content}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_get_cursor" => { + match get_nvim_cursor().await { + Ok(content) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": content } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to get cursor: {}", e)).await; + } } } - } - "nvim_get_visual_selection" => { - match get_nvim_visual_selection().await { - Ok(content) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": content}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_get_visual_selection" => { + match get_nvim_visual_selection().await { + Ok(content) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": content } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to get visual selection: {}", e)).await; + } } } - } - "nvim_set_diagnostics" => { - let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); - let message = args.get("message").and_then(|v| v.as_str()).unwrap_or(""); - - match set_nvim_diagnostics(line, message).await { - Ok(_) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}] - })), - error: None, - }); - } - Err(e) => { - send_response(JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id, - result: Some(json!({ - "isError": true, - "content": [{"type": "text", "text": format!("Error: {}", e)}] - })), - error: None, - }); + "nvim_set_diagnostics" => { + let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1); + let message = args.get("message").and_then(|v| v.as_str()).unwrap_or(""); + + match set_nvim_diagnostics(line, message).await { + Ok(_) => { + send_response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(json!({ + "content": [ + { "type": "text", "text": format!("Successfully set diagnostic on line {}", line) } + ] + })), + error: None, + }).await; + } + Err(e) => { + send_error(id, -32603, &format!("Failed to set diagnostic: {}", e)).await; + } } } - } - _ => { - send_error(id, -32601, "Tool not found"); + _ => { + send_error(id, -32601, "Tool not found").await; + } } } + _ => { + // Ignore other methods + } } - _ => { - // Ignore other methods - } - } + }); } } async fn get_socket_path() -> Result { + // 1. Try auto-discovery in \\.\pipe\ + if let Ok(dir) = std::fs::read_dir(r"\\.\pipe\") { + for entry in dir.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.starts_with("nvim.") { + return Ok(format!(r"\\.\pipe\{}", name_str)); + } + } + } + + // 2. Fallback to active_nvim.txt let profile = std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into()); let path = format!("{}\\.gemini\\active_nvim.txt", profile); @@ -249,36 +234,73 @@ async fn get_socket_path() -> Result { } } - Err("Could not find active Windows Neovim named pipe in active_nvim.txt".to_string()) + Err("Could not find active Windows Neovim named pipe".to_string()) } -async fn send_nvim_command(cmd: &str) -> Result<(), String> { +async fn call_nvim(req: rmpv::Value) -> Result { + let msgid = if let rmpv::Value::Array(ref arr) = req { + if arr.len() > 1 { arr[1].clone() } else { rmpv::Value::Nil } + } else { rmpv::Value::Nil }; + let socket_path = get_socket_path().await?; let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?; - use rmpv::Value as RmpValue; - let req = RmpValue::Array(vec![ - RmpValue::Integer(0.into()), - RmpValue::Integer(1.into()), - RmpValue::String("nvim_command".into()), - RmpValue::Array(vec![RmpValue::String(cmd.into())]), - ]); - let mut buf = Vec::new(); rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; client.write_all(&buf).await.map_err(|e| e.to_string())?; - Ok(()) + let mut resp_buf = Vec::new(); + let mut chunk = vec![0u8; 8192]; + let mut offset = 0; + + loop { + let mut cursor = std::io::Cursor::new(&resp_buf[offset..]); + match rmpv::decode::read_value(&mut cursor) { + Ok(val) => { + offset += cursor.position() as usize; + + if let rmpv::Value::Array(ref arr) = val { + if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid { + return Ok(val); + } + } + continue; + }, + Err(_) => { + let n = client.read(&mut chunk).await.map_err(|e| e.to_string())?; + if n == 0 { + return Err("Unexpected EOF from Neovim".to_string()); + } + resp_buf.extend_from_slice(&chunk[..n]); + } + } + } } -async fn get_nvim_active_buffer() -> Result { - let socket_path = get_socket_path().await?; - let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?; - +async fn send_nvim_command(cmd: &str) -> Result<(), String> { use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(2.into()), + RmpValue::Integer(1.into()), // msgid + RmpValue::String("nvim_command".into()), + RmpValue::Array(vec![RmpValue::String(cmd.into())]), + ]); + + let resp = call_nvim(req).await?; + if let RmpValue::Array(arr) = resp { + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + return Ok(()); + } + Err("Invalid response".to_string()) +} + +async fn get_nvim_active_buffer() -> Result { + use rmpv::Value as RmpValue; + let req = RmpValue::Array(vec![ + RmpValue::Integer(0.into()), + RmpValue::Integer(2.into()), // msgid RmpValue::String("nvim_buf_get_lines".into()), RmpValue::Array(vec![ RmpValue::Integer(0.into()), @@ -288,83 +310,55 @@ async fn get_nvim_active_buffer() -> Result { ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - client.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 1024 * 1024]; // 1MB buffer - let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - if let RmpValue::Array(lines) = &arr[3] { - let mut text = String::new(); - for line in lines { - if let RmpValue::String(s) = line { - if let Some(s) = s.as_str() { - text.push_str(s); - text.push('\n'); - } + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + if let RmpValue::Array(lines) = &arr[3] { + let mut text = String::new(); + for line in lines { + if let RmpValue::String(s) = line { + if let Some(s) = s.as_str() { + text.push_str(s); + text.push('\n'); } } - return Ok(text); } + return Ok(text); } } - - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } async fn get_nvim_cursor() -> Result { - let socket_path = get_socket_path().await?; - let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?; - use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(2.into()), + RmpValue::Integer(3.into()), // msgid RmpValue::String("nvim_win_get_cursor".into()), RmpValue::Array(vec![ RmpValue::Integer(0.into()), ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - client.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 8192]; - let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - if let RmpValue::Array(pos) = &arr[3] { - if pos.len() == 2 { - if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) { - return Ok(format!("Line: {}, Column: {}", row, col)); - } + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + if let RmpValue::Array(pos) = &arr[3] { + if pos.len() == 2 { + if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) { + return Ok(format!("Line: {}, Column: {}", row, col)); } } } } - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } async fn get_nvim_visual_selection() -> Result { - let socket_path = get_socket_path().await?; - let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?; - let lua_script = r#" local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) @@ -379,7 +373,7 @@ async fn get_nvim_visual_selection() -> Result { use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(3.into()), + RmpValue::Integer(4.into()), // msgid RmpValue::String("nvim_exec_lua".into()), RmpValue::Array(vec![ RmpValue::String(lua_script.into()), @@ -387,36 +381,22 @@ async fn get_nvim_visual_selection() -> Result { ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - client.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 1024 * 1024]; - let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - if let RmpValue::String(s) = &arr[3] { - if let Some(text) = s.as_str() { - return Ok(text.to_string()); - } + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); + } + if let RmpValue::String(s) = &arr[3] { + if let Some(text) = s.as_str() { + return Ok(text.to_string()); } } } - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> { - let socket_path = get_socket_path().await?; - let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?; - let escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\""); - let lua_script = format!(r#" local ns = vim.api.nvim_create_namespace("gemini_diagnostics") local diagnostics = {{{{ @@ -431,7 +411,7 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> { use rmpv::Value as RmpValue; let req = RmpValue::Array(vec![ RmpValue::Integer(0.into()), - RmpValue::Integer(4.into()), + RmpValue::Integer(5.into()), // msgid RmpValue::String("nvim_exec_lua".into()), RmpValue::Array(vec![ RmpValue::String(lua_script.into()), @@ -439,22 +419,12 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> { ]), ]); - let mut buf = Vec::new(); - rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; - client.write_all(&buf).await.map_err(|e| e.to_string())?; - - let mut resp_buf = [0u8; 8192]; - let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?; - let mut cursor = std::io::Cursor::new(&resp_buf[..n]); - let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?; - + let resp = call_nvim(req).await?; if let RmpValue::Array(arr) = resp { - if arr.len() >= 4 { - if !arr[2].is_nil() { - return Err(format!("Neovim error: {:?}", arr[2])); - } - return Ok(()); + if !arr[2].is_nil() { + return Err(format!("Neovim error: {:?}", arr[2])); } + return Ok(()); } - Err("Invalid response from Neovim".to_string()) + Err("Invalid response".to_string()) } diff --git a/win-nvim/src/mcp.rs b/win-nvim/src/mcp.rs index a837d57..51c7421 100644 --- a/win-nvim/src/mcp.rs +++ b/win-nvim/src/mcp.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct JsonRpcRequest { @@ -19,14 +20,12 @@ pub struct JsonRpcResponse { pub error: Option, } -pub fn read_message() -> Option { - use std::io::{BufRead, Read}; - let stdin = std::io::stdin(); - let mut handle = stdin.lock(); +pub async fn read_message() -> Option { + let mut stdin = BufReader::new(tokio::io::stdin()); let mut length = 0; loop { let mut line = String::new(); - if handle.read_line(&mut line).unwrap_or(0) == 0 { + if stdin.read_line(&mut line).await.unwrap_or(0) == 0 { return None; } let line = line.trim_end(); @@ -41,22 +40,23 @@ pub fn read_message() -> Option { return None; } let mut buffer = vec![0; length]; - handle.read_exact(&mut buffer).unwrap_or(()); + stdin.read_exact(&mut buffer).await.unwrap_or(0); serde_json::from_slice(&buffer).ok() } -pub fn send_response(response: JsonRpcResponse) { +pub async fn send_response(response: JsonRpcResponse) { let msg = serde_json::to_string(&response).unwrap(); - print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg); - use std::io::Write; - std::io::stdout().flush().unwrap(); + let payload = format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg); + let mut stdout = tokio::io::stdout(); + let _ = stdout.write_all(payload.as_bytes()).await; + let _ = stdout.flush().await; } -pub fn send_error(id: Value, code: i32, message: &str) { +pub async fn send_error(id: Value, code: i32, message: &str) { send_response(JsonRpcResponse { jsonrpc: "2.0".to_string(), id, result: None, error: Some(serde_json::json!({"code": code, "message": message})), - }); + }).await; }