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
This commit is contained in:
Riz Ashraf committed 2026-09-12 08:21:41 +01:00
1 parent 67b6a0407e
commit 1fd1d119e6
6 files changed
+252 -316

No files matched your search

+3 -3
View File
@@ -2,14 +2,14 @@
setlocal setlocal
echo Building mcp-memory (server and stub) for Windows... 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 ( if %ERRORLEVEL% NEQ 0 (
echo Windows build failed! echo Windows build failed!
exit /b %ERRORLEVEL% exit /b %ERRORLEVEL%
) )
echo Building mcp-memory (server and stub) for Linux (WSL)... 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 ( if %ERRORLEVEL% NEQ 0 (
echo Linux build failed! echo Linux build failed!
exit /b %ERRORLEVEL% exit /b %ERRORLEVEL%
@@ -31,7 +31,7 @@ echo.
echo Copying Windows executables... 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-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\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... 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" 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"
+104 -141
View File
@@ -8,12 +8,13 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
loop { loop {
let msg = match read_message() { let msg = match read_message().await {
Some(m) => m, Some(m) => m,
None => break, 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() { match msg.method.as_str() {
"initialize" => { "initialize" => {
@@ -28,7 +29,7 @@ async fn main() {
} }
})), })),
error: None, error: None,
}); }).await;
} }
"tools/list" => { "tools/list" => {
send_response(JsonRpcResponse { send_response(JsonRpcResponse {
@@ -87,10 +88,10 @@ async fn main() {
] ]
})), })),
error: None, error: None,
}); }).await;
} }
"tools/call" => { "tools/call" => {
let params = msg.params.unwrap_or(json!({})); let params = msg.params.clone().unwrap_or(json!({}));
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or(json!({})); let args = params.get("arguments").cloned().unwrap_or(json!({}));
@@ -99,27 +100,22 @@ async fn main() {
let file = args.get("file").and_then(|v| v.as_str()).unwrap_or(""); 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 line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
match send_nvim_command(&format!("edit +{} {}", line, file)).await { let cmd = format!("edit {} | {} | normal! zz", file, line);
match send_nvim_command(&cmd).await {
Ok(_) => { Ok(_) => {
send_response(JsonRpcResponse { send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}] "content": [
{ "type": "text", "text": format!("Successfully jumped to {}:{}", file, line) }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to execute command: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -130,21 +126,15 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": content}] "content": [
{ "type": "text", "text": content }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to get active buffer: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -155,21 +145,15 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": content}] "content": [
{ "type": "text", "text": content }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to get cursor: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -180,21 +164,15 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": content}] "content": [
{ "type": "text", "text": content }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to get visual selection: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -208,26 +186,20 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}] "content": [
{ "type": "text", "text": format!("Successfully set diagnostic on line {}", line) }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to set diagnostic: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
_ => { _ => {
send_error(id, -32601, "Tool not found"); send_error(id, -32601, "Tool not found").await;
} }
} }
} }
@@ -235,18 +207,23 @@ async fn main() {
// Ignore other methods // Ignore other methods
} }
} }
});
} }
} }
async fn get_socket_path() -> Result<String, String> { async fn get_socket_path() -> Result<String, String> {
if let Ok(content) = std::fs::read_to_string(format!("{}/.gemini/active_nvim.txt", std::env::var("HOME").unwrap_or("/home/riz".into()))) { // 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(); let p = content.trim().to_string();
if !p.is_empty() { if !p.is_empty() {
return Ok(p); return Ok(p);
} }
} }
}
// Fallback: search /tmp/nvim.*/0 // 2. Fallback: search /tmp/nvim.*/0
if let Ok(entries) = std::fs::read_dir("/tmp") { if let Ok(entries) = std::fs::read_dir("/tmp") {
for entry in entries.flatten() { for entry in entries.flatten() {
if let Ok(name) = entry.file_name().into_string() { if let Ok(name) = entry.file_name().into_string() {
@@ -262,33 +239,70 @@ async fn get_socket_path() -> Result<String, String> {
Err("Could not find Neovim socket".to_string()) 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<rmpv::Value, String> {
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 socket_path = get_socket_path().await?;
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?; 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(); let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
stream.write_all(&buf).await.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<String, String> { async fn send_nvim_command(cmd: &str) -> Result<(), String> {
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; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), 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<String, String> {
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::String("nvim_buf_get_lines".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
@@ -298,17 +312,8 @@ async fn get_nvim_active_buffer() -> Result<String, String> {
]), ]),
]); ]);
let mut buf = Vec::new(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
@@ -325,35 +330,22 @@ async fn get_nvim_active_buffer() -> Result<String, String> {
return Ok(text); return Ok(text);
} }
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
async fn get_nvim_cursor() -> Result<String, String> { async fn get_nvim_cursor() -> Result<String, String> {
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; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
RmpValue::Integer(2.into()), RmpValue::Integer(3.into()), // msgid
RmpValue::String("nvim_win_get_cursor".into()), RmpValue::String("nvim_win_get_cursor".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
]), ]),
]); ]);
let mut buf = Vec::new(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
@@ -365,14 +357,10 @@ async fn get_nvim_cursor() -> Result<String, String> {
} }
} }
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
async fn get_nvim_visual_selection() -> Result<String, String> { async fn get_nvim_visual_selection() -> Result<String, String> {
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#" let lua_script = r#"
local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<"))
local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>"))
@@ -387,7 +375,7 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
use rmpv::Value as RmpValue; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
RmpValue::Integer(3.into()), RmpValue::Integer(4.into()), // msgid
RmpValue::String("nvim_exec_lua".into()), RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::String(lua_script.into()), RmpValue::String(lua_script.into()),
@@ -395,17 +383,8 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
]), ]),
]); ]);
let mut buf = Vec::new(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
@@ -415,17 +394,11 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
} }
} }
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), 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 escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\"");
let lua_script = format!(r#" let lua_script = format!(r#"
local ns = vim.api.nvim_create_namespace("gemini_diagnostics") local ns = vim.api.nvim_create_namespace("gemini_diagnostics")
local diagnostics = {{{{ local diagnostics = {{{{
@@ -440,7 +413,7 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
use rmpv::Value as RmpValue; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
RmpValue::Integer(4.into()), RmpValue::Integer(5.into()), // msgid
RmpValue::String("nvim_exec_lua".into()), RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::String(lua_script.into()), 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(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
return Ok(()); return Ok(());
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
+12 -12
View File
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JsonRpcRequest { pub struct JsonRpcRequest {
@@ -19,14 +20,12 @@ pub struct JsonRpcResponse {
pub error: Option<Value>, pub error: Option<Value>,
} }
pub fn read_message() -> Option<JsonRpcRequest> { pub async fn read_message() -> Option<JsonRpcRequest> {
use std::io::{BufRead, Read}; let mut stdin = BufReader::new(tokio::io::stdin());
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut length = 0; let mut length = 0;
loop { loop {
let mut line = String::new(); 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; return None;
} }
let line = line.trim_end(); let line = line.trim_end();
@@ -41,22 +40,23 @@ pub fn read_message() -> Option<JsonRpcRequest> {
return None; return None;
} }
let mut buffer = vec![0; length]; 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() 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(); let msg = serde_json::to_string(&response).unwrap();
print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg); let payload = format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
use std::io::Write; let mut stdout = tokio::io::stdout();
std::io::stdout().flush().unwrap(); 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 { send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: None, result: None,
error: Some(serde_json::json!({"code": code, "message": message})), error: Some(serde_json::json!({"code": code, "message": message})),
}); }).await;
} }
+10 -7
View File
@@ -24,28 +24,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let (msg_tx, mut msg_rx) = mpsc::channel::<String>(100); let (msg_tx, mut msg_rx) = mpsc::channel::<String>(100);
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
tokio::task::spawn_blocking(move || { tokio::spawn(async move {
let stdin = std::io::stdin(); let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
let mut handle = stdin.lock();
let mut buffer = String::new(); 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 { if bytes == 0 {
break; break;
} }
let _ = msg_tx.blocking_send(buffer.clone()); let _ = msg_tx.send(buffer.clone()).await;
buffer.clear(); buffer.clear();
} }
let _ = shutdown_tx.blocking_send(()); let _ = shutdown_tx.send(()).await;
}); });
let target_url = cli.target; let target_url = cli.target;
let post_url = Arc::new(RwLock::new(String::new())); 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()?; let client = reqwest::Client::builder().build()?;
tokio::spawn(async move { tokio::spawn(async move {
while let Some(msg) = msg_rx.recv().await { while let Some(msg) = msg_rx.recv().await {
let post_url_clone = Arc::clone(&post_url_proxy);
let client = client.clone();
tokio::spawn(async move {
let mut attempts = 0; let mut attempts = 0;
loop { loop {
let url = post_url_clone.read().await.clone(); let url = post_url_clone.read().await.clone();
@@ -66,6 +68,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("[PROXY] Waiting for server to accept messages..."); eprintln!("[PROXY] Waiting for server to accept messages...");
} }
} }
});
} }
}); });
+111 -141
View File
@@ -8,12 +8,13 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
loop { loop {
let msg = match read_message() { let msg = match read_message().await {
Some(m) => m, Some(m) => m,
None => break, 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() { match msg.method.as_str() {
"initialize" => { "initialize" => {
@@ -28,7 +29,7 @@ async fn main() {
} }
})), })),
error: None, error: None,
}); }).await;
} }
"tools/list" => { "tools/list" => {
send_response(JsonRpcResponse { send_response(JsonRpcResponse {
@@ -87,10 +88,10 @@ async fn main() {
] ]
})), })),
error: None, error: None,
}); }).await;
} }
"tools/call" => { "tools/call" => {
let params = msg.params.unwrap_or(json!({})); let params = msg.params.clone().unwrap_or(json!({}));
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or(json!({})); let args = params.get("arguments").cloned().unwrap_or(json!({}));
@@ -99,27 +100,22 @@ async fn main() {
let file = args.get("file").and_then(|v| v.as_str()).unwrap_or(""); 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 line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
match send_nvim_command(&format!("edit +{} {}", line, file)).await { let cmd = format!("edit {} | {} | normal! zz", file, line);
match send_nvim_command(&cmd).await {
Ok(_) => { Ok(_) => {
send_response(JsonRpcResponse { send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}] "content": [
{ "type": "text", "text": format!("Successfully jumped to {}:{}", file, line) }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to execute command: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -130,21 +126,15 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": content}] "content": [
{ "type": "text", "text": content }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to get active buffer: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -155,21 +145,15 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": content}] "content": [
{ "type": "text", "text": content }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to get cursor: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -180,21 +164,15 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": content}] "content": [
{ "type": "text", "text": content }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to get visual selection: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
@@ -208,26 +186,20 @@ async fn main() {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: Some(json!({ result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}] "content": [
{ "type": "text", "text": format!("Successfully set diagnostic on line {}", line) }
]
})), })),
error: None, error: None,
}); }).await;
} }
Err(e) => { Err(e) => {
send_response(JsonRpcResponse { send_error(id, -32603, &format!("Failed to set diagnostic: {}", e)).await;
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
} }
} }
} }
_ => { _ => {
send_error(id, -32601, "Tool not found"); send_error(id, -32601, "Tool not found").await;
} }
} }
} }
@@ -235,10 +207,23 @@ async fn main() {
// Ignore other methods // Ignore other methods
} }
} }
});
} }
} }
async fn get_socket_path() -> Result<String, String> { async fn get_socket_path() -> Result<String, String> {
// 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 profile = std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
let path = format!("{}\\.gemini\\active_nvim.txt", profile); let path = format!("{}\\.gemini\\active_nvim.txt", profile);
@@ -249,36 +234,73 @@ async fn get_socket_path() -> Result<String, String> {
} }
} }
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<rmpv::Value, String> {
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 socket_path = get_socket_path().await?;
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?; 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(); let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?; rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
client.write_all(&buf).await.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<String, String> { async fn send_nvim_command(cmd: &str) -> Result<(), String> {
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; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), 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<String, String> {
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::String("nvim_buf_get_lines".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
@@ -288,18 +310,8 @@ async fn get_nvim_active_buffer() -> Result<String, String> {
]), ]),
]); ]);
let mut buf = Vec::new(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
@@ -316,36 +328,22 @@ async fn get_nvim_active_buffer() -> Result<String, String> {
return Ok(text); return Ok(text);
} }
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
async fn get_nvim_cursor() -> Result<String, String> { async fn get_nvim_cursor() -> Result<String, String> {
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; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
RmpValue::Integer(2.into()), RmpValue::Integer(3.into()), // msgid
RmpValue::String("nvim_win_get_cursor".into()), RmpValue::String("nvim_win_get_cursor".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
]), ]),
]); ]);
let mut buf = Vec::new(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
@@ -357,14 +355,10 @@ async fn get_nvim_cursor() -> Result<String, String> {
} }
} }
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
async fn get_nvim_visual_selection() -> Result<String, String> { async fn get_nvim_visual_selection() -> Result<String, String> {
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#" let lua_script = r#"
local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<"))
local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>"))
@@ -379,7 +373,7 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
use rmpv::Value as RmpValue; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
RmpValue::Integer(3.into()), RmpValue::Integer(4.into()), // msgid
RmpValue::String("nvim_exec_lua".into()), RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::String(lua_script.into()), RmpValue::String(lua_script.into()),
@@ -387,17 +381,8 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
]), ]),
]); ]);
let mut buf = Vec::new(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
@@ -407,16 +392,11 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
} }
} }
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), 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 escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\"");
let lua_script = format!(r#" let lua_script = format!(r#"
local ns = vim.api.nvim_create_namespace("gemini_diagnostics") local ns = vim.api.nvim_create_namespace("gemini_diagnostics")
local diagnostics = {{{{ local diagnostics = {{{{
@@ -431,7 +411,7 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
use rmpv::Value as RmpValue; use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![ let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()), RmpValue::Integer(0.into()),
RmpValue::Integer(4.into()), RmpValue::Integer(5.into()), // msgid
RmpValue::String("nvim_exec_lua".into()), RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![ RmpValue::Array(vec![
RmpValue::String(lua_script.into()), 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(); let resp = call_nvim(req).await?;
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())?;
if let RmpValue::Array(arr) = resp { if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() { if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2])); return Err(format!("Neovim error: {:?}", arr[2]));
} }
return Ok(()); return Ok(());
} }
} Err("Invalid response".to_string())
Err("Invalid response from Neovim".to_string())
} }
+12 -12
View File
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JsonRpcRequest { pub struct JsonRpcRequest {
@@ -19,14 +20,12 @@ pub struct JsonRpcResponse {
pub error: Option<Value>, pub error: Option<Value>,
} }
pub fn read_message() -> Option<JsonRpcRequest> { pub async fn read_message() -> Option<JsonRpcRequest> {
use std::io::{BufRead, Read}; let mut stdin = BufReader::new(tokio::io::stdin());
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut length = 0; let mut length = 0;
loop { loop {
let mut line = String::new(); 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; return None;
} }
let line = line.trim_end(); let line = line.trim_end();
@@ -41,22 +40,23 @@ pub fn read_message() -> Option<JsonRpcRequest> {
return None; return None;
} }
let mut buffer = vec![0; length]; 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() 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(); let msg = serde_json::to_string(&response).unwrap();
print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg); let payload = format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
use std::io::Write; let mut stdout = tokio::io::stdout();
std::io::stdout().flush().unwrap(); 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 { send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(), jsonrpc: "2.0".to_string(),
id, id,
result: None, result: None,
error: Some(serde_json::json!({"code": code, "message": message})), error: Some(serde_json::json!({"code": code, "message": message})),
}); }).await;
} }