Fix MCP hangs and add logging

This commit is contained in:
Riz Ashraf committed 2026-09-13 11:10:35 +01:00
1 parent 75748f0e21
commit f2582b58be
18 files changed
+1034 -39

No files matched your search

+185 -5
View File
@@ -5,10 +5,36 @@ use serde_json::json;
use tokio::net::UnixStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
let base_dir = std::env::var("MCP_MEMORY_STORE_DIR").unwrap_or_else(|_| {
dirs::home_dir()
.map(|mut h| {
h.push(".gemini/mcp_memory");
h.to_string_lossy().to_string()
})
.unwrap_or_else(|| ".gemini/mcp_memory".into())
});
let log_dir = std::path::PathBuf::from(base_dir).join("logs");
std::fs::create_dir_all(&log_dir).unwrap_or_default();
let file_appender = tracing_appender::rolling::daily(log_dir, format!("{}.log", app_name));
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let _ = tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_ansi(false)
.with_max_level(tracing::Level::INFO)
.try_init();
Some(guard)
}
#[tokio::main]
async fn main() {
let _guard = init_logging("linux-nvim");
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
loop {
let msg = match read_message().await {
let msg = match read_message(&mut stdin).await {
Some(m) => m,
None => break,
};
@@ -84,6 +110,33 @@ async fn main() {
},
"required": ["line", "message"]
}
},
{
"name": "nvim_execute_lua",
"description": "Execute arbitrary Lua code in Neovim and return the result (JSON serialized).",
"inputSchema": {
"type": "object",
"properties": {
"code": { "type": "string" }
},
"required": ["code"]
}
},
{
"name": "nvim_list_buffers",
"description": "Get a list of all loaded Neovim buffers and their IDs.",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_get_diagnostics",
"description": "Get all LSP diagnostics (errors, warnings) for the active buffer.",
"inputSchema": {
"type": "object",
"properties": {}
}
}
]
})),
@@ -198,6 +251,62 @@ async fn main() {
}
}
}
"nvim_execute_lua" => {
let code = args.get("code").and_then(|v| v.as_str()).unwrap_or("");
match execute_nvim_lua(code).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 execute lua: {}", e)).await; }
}
}
"nvim_list_buffers" => {
let code = r#"
local bufs = vim.api.nvim_list_bufs()
local loaded = {}
for _, b in ipairs(bufs) do
if vim.api.nvim_buf_is_loaded(b) then
local name = vim.api.nvim_buf_get_name(b)
table.insert(loaded, {id = b, name = name == "" and "[No Name]" or name})
end
end
return loaded
"#;
match execute_nvim_lua(code).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 list buffers: {}", e)).await; }
}
}
"nvim_get_diagnostics" => {
let code = r#"
local diags = vim.diagnostic.get(0)
local res = {}
for _, d in ipairs(diags) do
table.insert(res, {
line = d.lnum + 1,
col = d.col,
message = d.message,
severity = d.severity
})
end
return res
"#;
match execute_nvim_lua(code).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 diagnostics: {}", e)).await; }
}
}
_ => {
send_error(id, -32601, "Tool not found").await;
}
@@ -269,11 +378,15 @@ async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
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());
let read_future = stream.read(&mut chunk);
match tokio::time::timeout(tokio::time::Duration::from_secs(5), read_future).await {
Ok(Ok(n)) => {
if n == 0 { return Err("Connection closed".into()); }
resp_buf.extend_from_slice(&chunk[..n]);
}
Ok(Err(e)) => return Err(e.to_string()),
Err(_) => return Err("Timeout waiting for Neovim response".into()),
}
resp_buf.extend_from_slice(&chunk[..n]);
}
}
}
@@ -430,3 +543,70 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
}
Err("Invalid response".to_string())
}
fn rmpv_to_json(val: &rmpv::Value) -> serde_json::Value {
match val {
rmpv::Value::Nil => serde_json::Value::Null,
rmpv::Value::Boolean(b) => serde_json::json!(b),
rmpv::Value::Integer(i) => {
if let Some(n) = i.as_i64() {
serde_json::json!(n)
} else if let Some(n) = i.as_u64() {
serde_json::json!(n)
} else {
serde_json::Value::Null
}
},
rmpv::Value::F32(f) => serde_json::json!(f),
rmpv::Value::F64(f) => serde_json::json!(f),
rmpv::Value::String(s) => {
if let Some(str_val) = s.as_str() {
serde_json::json!(str_val)
} else {
serde_json::Value::Null
}
},
rmpv::Value::Array(arr) => {
let vec: Vec<serde_json::Value> = arr.iter().map(rmpv_to_json).collect();
serde_json::Value::Array(vec)
},
rmpv::Value::Map(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map {
let key_str = if let rmpv::Value::String(s) = k {
s.as_str().unwrap_or("").to_string()
} else {
format!("{:?}", k)
};
obj.insert(key_str, rmpv_to_json(v));
}
serde_json::Value::Object(obj)
},
_ => serde_json::json!(format!("{:?}", val)),
}
}
async fn execute_nvim_lua(code: &str) -> Result<String, String> {
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(6.into()), // msgid
RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![
RmpValue::String(code.into()),
RmpValue::Array(vec![]),
]),
]);
let resp = call_nvim(req).await?;
if let RmpValue::Array(arr) = resp {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
if arr.len() > 3 {
return Ok(serde_json::to_string_pretty(&rmpv_to_json(&arr[3])).unwrap_or_default());
}
return Ok("".to_string());
}
Err("Invalid response".to_string())
}