Fix MCP hangs and add logging
This commit is contained in:
1 parent
75748f0e21
commit
f2582b58be
18 files changed
+1034
-39
No files matched your search
@@ -10,3 +10,7 @@ rmp-serde = "1.1"
|
||||
rmpv = "1.0"
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
schemars = "0.8"
|
||||
tracing-appender = "0.2.5"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
dirs = "7.0.0"
|
||||
+185
-5
@@ -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())
|
||||
}
|
||||
@@ -20,8 +20,7 @@ pub struct JsonRpcResponse {
|
||||
pub error: Option<Value>,
|
||||
}
|
||||
|
||||
pub async fn read_message() -> Option<JsonRpcRequest> {
|
||||
let mut stdin = BufReader::new(tokio::io::stdin());
|
||||
pub async fn read_message(stdin: &mut BufReader<tokio::io::Stdin>) -> Option<JsonRpcRequest> {
|
||||
let mut length = 0;
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
|
||||
Reference in new issue
Block a user