chore: rustfmt, clippy lints and code tidying
This commit is contained in:
1 parent
3716c3e698
commit
1752753fcc
19 files changed
+1273
-738
No files matched your search
+14
-9
@@ -2,19 +2,24 @@ use std::process::Command;
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let git_hash = Command::new("git")
|
let git_hash = Command::new("git")
|
||||||
.args(&["rev-parse", "--short", "HEAD"])
|
.args(["rev-parse", "--short", "HEAD"])
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
|
|
||||||
let git_date = Command::new("git")
|
|
||||||
.args(&["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
let version = format!("{} ({} {})", env!("CARGO_PKG_VERSION"), git_date.trim(), git_hash.trim());
|
let git_date = Command::new("git")
|
||||||
|
.args(["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
let version = format!(
|
||||||
|
"{} ({} {})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
git_date.trim(),
|
||||||
|
git_hash.trim()
|
||||||
|
);
|
||||||
println!("cargo:rustc-env=APP_VERSION={}", version);
|
println!("cargo:rustc-env=APP_VERSION={}", version);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use serde_json::{json, Value};
|
use serde_json::Value;
|
||||||
use std::io::{BufRead, BufReader, Read, Write};
|
use std::io::{BufRead, BufReader, Read, Write};
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
fn send_message(stdin: &mut std::process::ChildStdin, msg: Value) {
|
fn send_message(stdin: &mut std::process::ChildStdin, msg: Value) {
|
||||||
let s = serde_json::to_string(&msg).unwrap();
|
let s = serde_json::to_string(&msg).unwrap();
|
||||||
@@ -12,7 +11,7 @@ fn send_message(stdin: &mut std::process::ChildStdin, msg: Value) {
|
|||||||
fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
|
fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
|
||||||
let mut reader = BufReader::new(stdout);
|
let mut reader = BufReader::new(stdout);
|
||||||
let mut length = 0;
|
let mut length = 0;
|
||||||
|
|
||||||
// Read headers
|
// Read headers
|
||||||
loop {
|
loop {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
@@ -27,16 +26,16 @@ fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
|
|||||||
length = len_str.parse().unwrap_or(0);
|
length = len_str.parse().unwrap_or(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read body
|
// Read body
|
||||||
let mut buf = vec![0u8; length];
|
let mut buf = vec![0u8; length];
|
||||||
reader.read_exact(&mut buf).unwrap();
|
reader.read_exact(&mut buf).unwrap();
|
||||||
let body_str = String::from_utf8_lossy(&buf);
|
let body_str = String::from_utf8_lossy(&buf);
|
||||||
|
|
||||||
Some(serde_json::from_str(&body_str).unwrap())
|
Some(serde_json::from_str(&body_str).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +45,10 @@ fn test_mcp_initialization_and_tools_list() {
|
|||||||
let mut nvim_exe = std::env::current_exe().unwrap();
|
let mut nvim_exe = std::env::current_exe().unwrap();
|
||||||
nvim_exe.pop();
|
nvim_exe.pop();
|
||||||
nvim_exe.pop();
|
nvim_exe.pop();
|
||||||
nvim_exe.push(format!("mcp-memory-linux-nvim{}", std::env::consts::EXE_SUFFIX));
|
nvim_exe.push(format!(
|
||||||
|
"mcp-memory-linux-nvim{}",
|
||||||
|
std::env::consts::EXE_SUFFIX
|
||||||
|
));
|
||||||
|
|
||||||
let mut child = Command::new(&nvim_exe)
|
let mut child = Command::new(&nvim_exe)
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
@@ -88,12 +90,12 @@ fn test_mcp_initialization_and_tools_list() {
|
|||||||
let s = serde_json::to_string(&init_req).unwrap();
|
let s = serde_json::to_string(&init_req).unwrap();
|
||||||
stdin.write_all(format!("{}\n", s).as_bytes()).unwrap();
|
stdin.write_all(format!("{}\n", s).as_bytes()).unwrap();
|
||||||
stdin.flush().unwrap();
|
stdin.flush().unwrap();
|
||||||
|
|
||||||
let init_resp = read_message(&mut stdout).expect("Failed to read initialize response");
|
let init_resp = read_message(&mut stdout).expect("Failed to read initialize response");
|
||||||
|
|
||||||
assert_eq!(init_resp["jsonrpc"], "2.0");
|
assert_eq!(init_resp["jsonrpc"], "2.0");
|
||||||
assert_eq!(init_resp["id"], 1);
|
assert_eq!(init_resp["id"], 1);
|
||||||
|
|
||||||
// Verify capabilities
|
// Verify capabilities
|
||||||
let capabilities = &init_resp["result"]["capabilities"];
|
let capabilities = &init_resp["result"]["capabilities"];
|
||||||
assert_eq!(capabilities["tools"], serde_json::json!({}));
|
assert_eq!(capabilities["tools"], serde_json::json!({}));
|
||||||
@@ -105,17 +107,19 @@ fn test_mcp_initialization_and_tools_list() {
|
|||||||
"params": {},
|
"params": {},
|
||||||
"id": 2
|
"id": 2
|
||||||
});
|
});
|
||||||
|
|
||||||
send_message(&mut stdin, tools_req);
|
send_message(&mut stdin, tools_req);
|
||||||
|
|
||||||
let tools_resp = read_message(&mut stdout).expect("Failed to read tools/list response");
|
let tools_resp = read_message(&mut stdout).expect("Failed to read tools/list response");
|
||||||
|
|
||||||
assert_eq!(tools_resp["jsonrpc"], "2.0");
|
assert_eq!(tools_resp["jsonrpc"], "2.0");
|
||||||
assert_eq!(tools_resp["id"], 2);
|
assert_eq!(tools_resp["id"], 2);
|
||||||
|
|
||||||
let tools = tools_resp["result"]["tools"].as_array().expect("result.tools must be an array");
|
let tools = tools_resp["result"]["tools"]
|
||||||
|
.as_array()
|
||||||
|
.expect("result.tools must be an array");
|
||||||
assert!(!tools.is_empty(), "Server must expose at least one tool");
|
assert!(!tools.is_empty(), "Server must expose at least one tool");
|
||||||
|
|
||||||
let has_get_active_buffer = tools.iter().any(|t| t["name"] == "nvim_get_active_buffer");
|
let has_get_active_buffer = tools.iter().any(|t| t["name"] == "nvim_get_active_buffer");
|
||||||
assert!(has_get_active_buffer, "Missing nvim_get_active_buffer tool");
|
assert!(has_get_active_buffer, "Missing nvim_get_active_buffer tool");
|
||||||
|
|
||||||
|
|||||||
+169
-108
@@ -20,19 +20,25 @@ pub struct JsonRpcResponse {
|
|||||||
pub error: Option<Value>,
|
pub error: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_message<R: tokio::io::AsyncRead + Unpin>(stdin: &mut BufReader<R>) -> Option<JsonRpcRequest> {
|
pub async fn read_message<R: tokio::io::AsyncRead + Unpin>(
|
||||||
|
stdin: &mut BufReader<R>,
|
||||||
|
) -> Option<JsonRpcRequest> {
|
||||||
let mut length = 0;
|
let mut length = 0;
|
||||||
loop {
|
loop {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
if stdin.read_line(&mut line).await.unwrap_or(0) == 0 {
|
if stdin.read_line(&mut line).await.unwrap_or(0) == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if line.starts_with('{') {
|
if line.starts_with('{') {
|
||||||
return match serde_json::from_str::<JsonRpcRequest>(line.trim_end()) {
|
return match serde_json::from_str::<JsonRpcRequest>(line.trim_end()) {
|
||||||
Ok(req) => Some(req),
|
Ok(req) => Some(req),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to parse JSON-RPC request from JSONL: {}. Payload: {}", e, line);
|
tracing::error!(
|
||||||
|
"Failed to parse JSON-RPC request from JSONL: {}. Payload: {}",
|
||||||
|
e,
|
||||||
|
line
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -52,13 +58,21 @@ pub async fn read_message<R: tokio::io::AsyncRead + Unpin>(stdin: &mut BufReader
|
|||||||
}
|
}
|
||||||
let mut buffer = vec![0; length];
|
let mut buffer = vec![0; length];
|
||||||
stdin.read_exact(&mut buffer).await.unwrap_or(0);
|
stdin.read_exact(&mut buffer).await.unwrap_or(0);
|
||||||
|
|
||||||
serde_json::from_slice(&buffer).ok()
|
serde_json::from_slice(&buffer).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async 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();
|
||||||
tracing::info!("Sending JSON-RPC response (id: {:?}): {}", response.id, if msg.len() > 500 { format!("{}...", &msg[..500]) } else { msg.clone() });
|
tracing::info!(
|
||||||
|
"Sending JSON-RPC response (id: {:?}): {}",
|
||||||
|
response.id,
|
||||||
|
if msg.len() > 500 {
|
||||||
|
format!("{}...", &msg[..500])
|
||||||
|
} else {
|
||||||
|
msg.clone()
|
||||||
|
}
|
||||||
|
);
|
||||||
// CRITICAL ARCHITECTURAL DECISION:
|
// CRITICAL ARCHITECTURAL DECISION:
|
||||||
// The MCP StdioTransport MUST use Newline-Delimited JSON (NDJSON).
|
// The MCP StdioTransport MUST use Newline-Delimited JSON (NDJSON).
|
||||||
// Do NOT use LSP-style HTTP headers (e.g. Content-Length).
|
// Do NOT use LSP-style HTTP headers (e.g. Content-Length).
|
||||||
@@ -75,14 +89,16 @@ pub async fn send_error(id: Value, code: i32, message: &str) {
|
|||||||
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;
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
async fn get_socket_path() -> Result<String, String> {
|
async fn get_socket_path() -> Result<String, String> {
|
||||||
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);
|
||||||
|
|
||||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
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() {
|
||||||
@@ -120,7 +136,7 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
@@ -138,20 +154,28 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
||||||
use tokio::net::windows::named_pipe::ClientOptions;
|
use tokio::net::windows::named_pipe::ClientOptions;
|
||||||
|
|
||||||
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
||||||
if arr.len() > 1 { arr[1].clone() } else { rmpv::Value::Nil }
|
if arr.len() > 1 {
|
||||||
} else { rmpv::Value::Nil };
|
arr[1].clone()
|
||||||
|
} else {
|
||||||
|
rmpv::Value::Nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rmpv::Value::Nil
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!("Connecting to neovim pipe");
|
tracing::info!("Connecting to neovim pipe");
|
||||||
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())?;
|
||||||
|
|
||||||
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())?;
|
||||||
tracing::info!("Sending RPC request to neovim (msgid: {})", msgid);
|
tracing::info!("Sending RPC request to neovim (msgid: {})", msgid);
|
||||||
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let mut resp_buf = Vec::new();
|
let mut resp_buf = Vec::new();
|
||||||
let mut chunk = vec![0u8; 8192];
|
let mut chunk = vec![0u8; 8192];
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
@@ -161,20 +185,23 @@ async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
|||||||
match rmpv::decode::read_value(&mut cursor) {
|
match rmpv::decode::read_value(&mut cursor) {
|
||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
offset += cursor.position() as usize;
|
offset += cursor.position() as usize;
|
||||||
|
|
||||||
if let rmpv::Value::Array(ref arr) = val {
|
if let rmpv::Value::Array(ref arr) = val {
|
||||||
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid {
|
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid
|
||||||
|
{
|
||||||
tracing::info!("Received RPC response from neovim (msgid: {})", msgid);
|
tracing::info!("Received RPC response from neovim (msgid: {})", msgid);
|
||||||
return Ok(val);
|
return Ok(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let read_future = client.read(&mut chunk);
|
let read_future = client.read(&mut chunk);
|
||||||
match tokio::time::timeout(tokio::time::Duration::from_secs(5), read_future).await {
|
match tokio::time::timeout(tokio::time::Duration::from_secs(5), read_future).await {
|
||||||
Ok(Ok(n)) => {
|
Ok(Ok(n)) => {
|
||||||
if n == 0 { return Err("Connection closed".into()); }
|
if n == 0 {
|
||||||
|
return Err("Connection closed".into());
|
||||||
|
}
|
||||||
resp_buf.extend_from_slice(&chunk[..n]);
|
resp_buf.extend_from_slice(&chunk[..n]);
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => return Err(e.to_string()),
|
Ok(Err(e)) => return Err(e.to_string()),
|
||||||
@@ -193,18 +220,26 @@ async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
|||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
|
|
||||||
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
||||||
if arr.len() > 1 { arr[1].clone() } else { rmpv::Value::Nil }
|
if arr.len() > 1 {
|
||||||
} else { rmpv::Value::Nil };
|
arr[1].clone()
|
||||||
|
} else {
|
||||||
|
rmpv::Value::Nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rmpv::Value::Nil
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!("Connecting to neovim socket");
|
tracing::info!("Connecting to neovim socket");
|
||||||
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())?;
|
||||||
|
|
||||||
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())?;
|
||||||
tracing::info!("Sending RPC request to neovim (msgid: {})", msgid);
|
tracing::info!("Sending RPC request to neovim (msgid: {})", msgid);
|
||||||
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let mut resp_buf = Vec::new();
|
let mut resp_buf = Vec::new();
|
||||||
let mut chunk = vec![0u8; 8192];
|
let mut chunk = vec![0u8; 8192];
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
@@ -214,20 +249,23 @@ async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
|||||||
match rmpv::decode::read_value(&mut cursor) {
|
match rmpv::decode::read_value(&mut cursor) {
|
||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
offset += cursor.position() as usize;
|
offset += cursor.position() as usize;
|
||||||
|
|
||||||
if let rmpv::Value::Array(ref arr) = val {
|
if let rmpv::Value::Array(ref arr) = val {
|
||||||
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid {
|
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid
|
||||||
|
{
|
||||||
tracing::info!("Received RPC response from neovim (msgid: {})", msgid);
|
tracing::info!("Received RPC response from neovim (msgid: {})", msgid);
|
||||||
return Ok(val);
|
return Ok(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let read_future = stream.read(&mut chunk);
|
let read_future = stream.read(&mut chunk);
|
||||||
match tokio::time::timeout(tokio::time::Duration::from_secs(5), read_future).await {
|
match tokio::time::timeout(tokio::time::Duration::from_secs(5), read_future).await {
|
||||||
Ok(Ok(n)) => {
|
Ok(Ok(n)) => {
|
||||||
if n == 0 { return Err("Connection closed".into()); }
|
if n == 0 {
|
||||||
|
return Err("Connection closed".into());
|
||||||
|
}
|
||||||
resp_buf.extend_from_slice(&chunk[..n]);
|
resp_buf.extend_from_slice(&chunk[..n]);
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => return Err(e.to_string()),
|
Ok(Err(e)) => return Err(e.to_string()),
|
||||||
@@ -248,7 +286,7 @@ async fn send_nvim_command(cmd: &str) -> Result<(), String> {
|
|||||||
RmpValue::String("nvim_command".into()),
|
RmpValue::String("nvim_command".into()),
|
||||||
RmpValue::Array(vec![RmpValue::String(cmd.into())]),
|
RmpValue::Array(vec![RmpValue::String(cmd.into())]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
let resp = call_nvim(req).await?;
|
||||||
if let RmpValue::Array(arr) = resp {
|
if let RmpValue::Array(arr) = resp {
|
||||||
if !arr[2].is_nil() {
|
if !arr[2].is_nil() {
|
||||||
@@ -272,7 +310,7 @@ async fn get_nvim_active_buffer() -> Result<String, String> {
|
|||||||
RmpValue::Boolean(true),
|
RmpValue::Boolean(true),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
let resp = call_nvim(req).await?;
|
||||||
if let RmpValue::Array(arr) = resp {
|
if let RmpValue::Array(arr) = resp {
|
||||||
if !arr[2].is_nil() {
|
if !arr[2].is_nil() {
|
||||||
@@ -300,11 +338,9 @@ async fn get_nvim_cursor() -> Result<String, String> {
|
|||||||
RmpValue::Integer(0.into()),
|
RmpValue::Integer(0.into()),
|
||||||
RmpValue::Integer(3.into()), // msgid
|
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 resp = call_nvim(req).await?;
|
let resp = call_nvim(req).await?;
|
||||||
if let RmpValue::Array(arr) = resp {
|
if let RmpValue::Array(arr) = resp {
|
||||||
if !arr[2].is_nil() {
|
if !arr[2].is_nil() {
|
||||||
@@ -332,7 +368,7 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
|
|||||||
return lines
|
return lines
|
||||||
end
|
end
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
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()),
|
||||||
@@ -343,7 +379,7 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
|
|||||||
RmpValue::Array(vec![]),
|
RmpValue::Array(vec![]),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
let resp = call_nvim(req).await?;
|
||||||
if let RmpValue::Array(arr) = resp {
|
if let RmpValue::Array(arr) = resp {
|
||||||
if !arr[2].is_nil() {
|
if !arr[2].is_nil() {
|
||||||
@@ -360,7 +396,8 @@ async fn get_nvim_visual_selection() -> Result<String, String> {
|
|||||||
|
|
||||||
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), 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 = {{{{
|
||||||
lnum = {} - 1,
|
lnum = {} - 1,
|
||||||
@@ -369,8 +406,10 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
|||||||
message = "{}",
|
message = "{}",
|
||||||
}}}}
|
}}}}
|
||||||
vim.diagnostic.set(ns, 0, diagnostics, {{}})
|
vim.diagnostic.set(ns, 0, diagnostics, {{}})
|
||||||
"#, line, escaped_message);
|
"#,
|
||||||
|
line, escaped_message
|
||||||
|
);
|
||||||
|
|
||||||
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()),
|
||||||
@@ -381,7 +420,7 @@ async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
|||||||
RmpValue::Array(vec![]),
|
RmpValue::Array(vec![]),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
let resp = call_nvim(req).await?;
|
||||||
if let RmpValue::Array(arr) = resp {
|
if let RmpValue::Array(arr) = resp {
|
||||||
if !arr[2].is_nil() {
|
if !arr[2].is_nil() {
|
||||||
@@ -404,7 +443,7 @@ fn rmpv_to_json(val: &rmpv::Value) -> serde_json::Value {
|
|||||||
} else {
|
} else {
|
||||||
serde_json::Value::Null
|
serde_json::Value::Null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
rmpv::Value::F32(f) => serde_json::json!(f),
|
rmpv::Value::F32(f) => serde_json::json!(f),
|
||||||
rmpv::Value::F64(f) => serde_json::json!(f),
|
rmpv::Value::F64(f) => serde_json::json!(f),
|
||||||
rmpv::Value::String(s) => {
|
rmpv::Value::String(s) => {
|
||||||
@@ -413,11 +452,11 @@ fn rmpv_to_json(val: &rmpv::Value) -> serde_json::Value {
|
|||||||
} else {
|
} else {
|
||||||
serde_json::Value::Null
|
serde_json::Value::Null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
rmpv::Value::Array(arr) => {
|
rmpv::Value::Array(arr) => {
|
||||||
let vec: Vec<serde_json::Value> = arr.iter().map(rmpv_to_json).collect();
|
let vec: Vec<serde_json::Value> = arr.iter().map(rmpv_to_json).collect();
|
||||||
serde_json::Value::Array(vec)
|
serde_json::Value::Array(vec)
|
||||||
},
|
}
|
||||||
rmpv::Value::Map(map) => {
|
rmpv::Value::Map(map) => {
|
||||||
let mut obj = serde_json::Map::new();
|
let mut obj = serde_json::Map::new();
|
||||||
for (k, v) in map {
|
for (k, v) in map {
|
||||||
@@ -429,7 +468,7 @@ fn rmpv_to_json(val: &rmpv::Value) -> serde_json::Value {
|
|||||||
obj.insert(key_str, rmpv_to_json(v));
|
obj.insert(key_str, rmpv_to_json(v));
|
||||||
}
|
}
|
||||||
serde_json::Value::Object(obj)
|
serde_json::Value::Object(obj)
|
||||||
},
|
}
|
||||||
_ => serde_json::json!(format!("{:?}", val)),
|
_ => serde_json::json!(format!("{:?}", val)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,12 +479,9 @@ async fn execute_nvim_lua(code: &str) -> Result<String, String> {
|
|||||||
RmpValue::Integer(0.into()),
|
RmpValue::Integer(0.into()),
|
||||||
RmpValue::Integer(6.into()), // msgid
|
RmpValue::Integer(6.into()), // msgid
|
||||||
RmpValue::String("nvim_exec_lua".into()),
|
RmpValue::String("nvim_exec_lua".into()),
|
||||||
RmpValue::Array(vec![
|
RmpValue::Array(vec![RmpValue::String(code.into()), RmpValue::Array(vec![])]),
|
||||||
RmpValue::String(code.into()),
|
|
||||||
RmpValue::Array(vec![]),
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let resp = call_nvim(req).await?;
|
let resp = call_nvim(req).await?;
|
||||||
if let RmpValue::Array(arr) = resp {
|
if let RmpValue::Array(arr) = resp {
|
||||||
if !arr[2].is_nil() {
|
if !arr[2].is_nil() {
|
||||||
@@ -460,7 +496,12 @@ async fn execute_nvim_lua(code: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
||||||
if std::env::args().any(|arg| arg == "--version") {
|
if std::env::args().any(|arg| arg == "--version") {
|
||||||
println!("{} {} ({})", app_name, app_version, std::env::var("GIT_HASH").unwrap_or_else(|_| "unknown".to_string()));
|
println!(
|
||||||
|
"{} {} ({})",
|
||||||
|
app_name,
|
||||||
|
app_version,
|
||||||
|
std::env::var("GIT_HASH").unwrap_or_else(|_| "unknown".to_string())
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let _guard = init_logging(app_name);
|
let _guard = init_logging(app_name);
|
||||||
@@ -471,7 +512,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
Some(m) => {
|
Some(m) => {
|
||||||
tracing::info!("Received message method: {}", m.method);
|
tracing::info!("Received message method: {}", m.method);
|
||||||
m
|
m
|
||||||
},
|
}
|
||||||
None => {
|
None => {
|
||||||
tracing::info!("Stdin closed, exiting loop");
|
tracing::info!("Stdin closed, exiting loop");
|
||||||
break;
|
break;
|
||||||
@@ -488,9 +529,14 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
match msg.method.as_str() {
|
match msg.method.as_str() {
|
||||||
"initialize" => {
|
"initialize" => {
|
||||||
let init = rmcp::model::InitializeResult::new(
|
let init = rmcp::model::InitializeResult::new(
|
||||||
rmcp::model::ServerCapabilities::builder().enable_tools().build()
|
rmcp::model::ServerCapabilities::builder()
|
||||||
|
.enable_tools()
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
.with_server_info(rmcp::model::Implementation::new(app_name.clone(), app_version.clone()))
|
.with_server_info(rmcp::model::Implementation::new(
|
||||||
|
app_name.clone(),
|
||||||
|
app_version.clone(),
|
||||||
|
))
|
||||||
.with_protocol_version(rmcp::model::ProtocolVersion::V_2024_11_05);
|
.with_protocol_version(rmcp::model::ProtocolVersion::V_2024_11_05);
|
||||||
|
|
||||||
send_response(JsonRpcResponse {
|
send_response(JsonRpcResponse {
|
||||||
@@ -498,7 +544,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
id,
|
id,
|
||||||
result: Some(serde_json::to_value(init).unwrap()),
|
result: Some(serde_json::to_value(init).unwrap()),
|
||||||
error: None,
|
error: None,
|
||||||
}).await;
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
"notifications/initialized" => {}
|
"notifications/initialized" => {}
|
||||||
"tools/list" => {
|
"tools/list" => {
|
||||||
@@ -595,7 +642,10 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
|
|
||||||
match name {
|
match name {
|
||||||
"nvim_goto_line" => {
|
"nvim_goto_line" => {
|
||||||
if let (Some(file), Some(line)) = (args.get("file").and_then(|v| v.as_str()), args.get("line").and_then(|v| v.as_i64())) {
|
if let (Some(file), Some(line)) = (
|
||||||
|
args.get("file").and_then(|v| v.as_str()),
|
||||||
|
args.get("line").and_then(|v| v.as_i64()),
|
||||||
|
) {
|
||||||
let escaped_file = file.replace("\\", "\\\\");
|
let escaped_file = file.replace("\\", "\\\\");
|
||||||
let cmd = format!("e {} | {} | normal! zz", escaped_file, line);
|
let cmd = format!("e {} | {} | normal! zz", escaped_file, line);
|
||||||
match send_nvim_command(&cmd).await {
|
match send_nvim_command(&cmd).await {
|
||||||
@@ -615,53 +665,53 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
send_error(id, -32602, "Missing 'file' or 'line'").await;
|
send_error(id, -32602, "Missing 'file' or 'line'").await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_get_active_buffer" => {
|
"nvim_get_active_buffer" => match get_nvim_active_buffer().await {
|
||||||
match get_nvim_active_buffer().await {
|
Ok(content) => {
|
||||||
Ok(content) => {
|
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": content}]
|
||||||
"content": [{"type": "text", "text": content}]
|
})),
|
||||||
})),
|
error: None,
|
||||||
error: None,
|
})
|
||||||
}).await;
|
.await;
|
||||||
}
|
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
|
||||||
}
|
}
|
||||||
}
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
"nvim_get_cursor" => {
|
},
|
||||||
match get_nvim_cursor().await {
|
"nvim_get_cursor" => match get_nvim_cursor().await {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
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": content}]
|
"content": [{"type": "text", "text": content}]
|
||||||
})),
|
})),
|
||||||
error: None,
|
error: None,
|
||||||
}).await;
|
})
|
||||||
}
|
.await;
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
|
||||||
}
|
}
|
||||||
}
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
"nvim_get_visual_selection" => {
|
},
|
||||||
match get_nvim_visual_selection().await {
|
"nvim_get_visual_selection" => match get_nvim_visual_selection().await {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
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": content}]
|
"content": [{"type": "text", "text": content}]
|
||||||
})),
|
})),
|
||||||
error: None,
|
error: None,
|
||||||
}).await;
|
})
|
||||||
}
|
.await;
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
|
||||||
}
|
}
|
||||||
}
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
|
},
|
||||||
"nvim_set_diagnostics" => {
|
"nvim_set_diagnostics" => {
|
||||||
if let (Some(line), Some(message)) = (args.get("line").and_then(|v| v.as_i64()), args.get("message").and_then(|v| v.as_str())) {
|
if let (Some(line), Some(message)) = (
|
||||||
|
args.get("line").and_then(|v| v.as_i64()),
|
||||||
|
args.get("message").and_then(|v| v.as_str()),
|
||||||
|
) {
|
||||||
match set_nvim_diagnostics(line, message).await {
|
match set_nvim_diagnostics(line, message).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
send_response(JsonRpcResponse {
|
send_response(JsonRpcResponse {
|
||||||
@@ -700,7 +750,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
"content": [{"type": "text", "text": result}]
|
"content": [{"type": "text", "text": result}]
|
||||||
})),
|
})),
|
||||||
error: None,
|
error: None,
|
||||||
}).await;
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -730,7 +781,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
"content": [{"type": "text", "text": result}]
|
"content": [{"type": "text", "text": result}]
|
||||||
})),
|
})),
|
||||||
error: None,
|
error: None,
|
||||||
}).await;
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -746,7 +798,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
"content": [{"type": "text", "text": result}]
|
"content": [{"type": "text", "text": result}]
|
||||||
})),
|
})),
|
||||||
error: None,
|
error: None,
|
||||||
}).await;
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -770,18 +823,20 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init_logging(app_name: &str) -> tracing_appender::non_blocking::WorkerGuard {
|
fn init_logging(app_name: &str) -> tracing_appender::non_blocking::WorkerGuard {
|
||||||
let log_dir = dirs::home_dir().unwrap_or_default().join(".gemini/mcp_memory/logs");
|
let log_dir = dirs::home_dir()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.join(".gemini/mcp_memory/logs");
|
||||||
std::fs::create_dir_all(&log_dir).unwrap_or_default();
|
std::fs::create_dir_all(&log_dir).unwrap_or_default();
|
||||||
|
|
||||||
let file_appender = tracing_appender::rolling::daily(log_dir, format!("{}.log", app_name));
|
let file_appender = tracing_appender::rolling::daily(log_dir, format!("{}.log", app_name));
|
||||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
|
||||||
let _ = tracing_subscriber::fmt()
|
let _ = tracing_subscriber::fmt()
|
||||||
.with_writer(non_blocking)
|
.with_writer(non_blocking)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.with_max_level(tracing::Level::INFO)
|
.with_max_level(tracing::Level::INFO)
|
||||||
.try_init();
|
.try_init();
|
||||||
|
|
||||||
guard
|
guard
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,7 +849,10 @@ mod tests {
|
|||||||
assert_eq!(rmpv_to_json(&rmpv::Value::Nil), serde_json::Value::Null);
|
assert_eq!(rmpv_to_json(&rmpv::Value::Nil), serde_json::Value::Null);
|
||||||
assert_eq!(rmpv_to_json(&rmpv::Value::Boolean(true)), json!(true));
|
assert_eq!(rmpv_to_json(&rmpv::Value::Boolean(true)), json!(true));
|
||||||
assert_eq!(rmpv_to_json(&rmpv::Value::Integer(42.into())), json!(42));
|
assert_eq!(rmpv_to_json(&rmpv::Value::Integer(42.into())), json!(42));
|
||||||
assert_eq!(rmpv_to_json(&rmpv::Value::String("hello".into())), json!("hello"));
|
assert_eq!(
|
||||||
|
rmpv_to_json(&rmpv::Value::String("hello".into())),
|
||||||
|
json!("hello")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -809,9 +867,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_rmpv_to_json_map() {
|
fn test_rmpv_to_json_map() {
|
||||||
let mut map = vec![];
|
let mut map = vec![];
|
||||||
map.push((rmpv::Value::String("key1".into()), rmpv::Value::Integer(100.into())));
|
map.push((
|
||||||
|
rmpv::Value::String("key1".into()),
|
||||||
|
rmpv::Value::Integer(100.into()),
|
||||||
|
));
|
||||||
let rmp_map = rmpv::Value::Map(map);
|
let rmp_map = rmpv::Value::Map(map);
|
||||||
|
|
||||||
let json_map = rmpv_to_json(&rmp_map);
|
let json_map = rmpv_to_json(&rmp_map);
|
||||||
assert_eq!(json_map, json!({ "key1": 100 }));
|
assert_eq!(json_map, json!({ "key1": 100 }));
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-9
@@ -2,19 +2,24 @@ use std::process::Command;
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let git_hash = Command::new("git")
|
let git_hash = Command::new("git")
|
||||||
.args(&["rev-parse", "--short", "HEAD"])
|
.args(["rev-parse", "--short", "HEAD"])
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
|
|
||||||
let git_date = Command::new("git")
|
|
||||||
.args(&["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
let version = format!("{} ({} {})", env!("CARGO_PKG_VERSION"), git_date.trim(), git_hash.trim());
|
let git_date = Command::new("git")
|
||||||
|
.args(["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
let version = format!(
|
||||||
|
"{} ({} {})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
git_date.trim(),
|
||||||
|
git_hash.trim()
|
||||||
|
);
|
||||||
println!("cargo:rustc-env=APP_VERSION={}", version);
|
println!("cargo:rustc-env=APP_VERSION={}", version);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
use rmcp::model::{InitializeResult, ServerCapabilities};
|
use rmcp::model::{InitializeResult, ServerCapabilities};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let init = InitializeResult::new(
|
let init = InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
|
||||||
ServerCapabilities::builder().enable_tools().build()
|
.with_server_info(rmcp::model::Implementation::new(
|
||||||
).with_server_info(rmcp::model::Implementation::new("gemini-mcp-memory", "3.0.0"));
|
"gemini-mcp-memory",
|
||||||
|
"3.0.0",
|
||||||
|
));
|
||||||
println!("{}", serde_json::to_string_pretty(&init).unwrap());
|
println!("{}", serde_json::to_string_pretty(&init).unwrap());
|
||||||
}
|
}
|
||||||
+533
-218
File diff suppressed because it is too large.
Load diff
+299
-235
@@ -1,4 +1,7 @@
|
|||||||
#![cfg_attr(not(target_os = "windows"), allow(dead_code, unused_imports, unreachable_code))]
|
#![cfg_attr(
|
||||||
|
not(target_os = "windows"),
|
||||||
|
allow(dead_code, unused_imports, unreachable_code)
|
||||||
|
)]
|
||||||
|
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod mcp;
|
mod mcp;
|
||||||
@@ -13,11 +16,11 @@ use crate::models::*;
|
|||||||
use crate::state::MemoryState;
|
use crate::state::MemoryState;
|
||||||
use crate::store::Store;
|
use crate::store::Store;
|
||||||
|
|
||||||
|
use redb::ReadableTable;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use redb::ReadableTable;
|
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
@@ -83,11 +86,10 @@ enum GateCommands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn reconcile_worker(state: Arc<MemoryState>) {
|
async fn reconcile_worker(state: Arc<MemoryState>) {
|
||||||
loop {
|
loop {
|
||||||
sleep(Duration::from_secs(5)).await;
|
sleep(Duration::from_secs(5)).await;
|
||||||
|
|
||||||
let has_local = {
|
let has_local = {
|
||||||
let session = state.graph.read();
|
let session = state.graph.read();
|
||||||
!session.entities.is_empty() || !session.relations.is_empty()
|
!session.entities.is_empty() || !session.relations.is_empty()
|
||||||
@@ -106,14 +108,18 @@ async fn reconcile_worker(state: Arc<MemoryState>) {
|
|||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
let _ = tokio::task::spawn_blocking(move || {
|
let _ = tokio::task::spawn_blocking(move || {
|
||||||
state_clone.rebuild_index();
|
state_clone.rebuild_index();
|
||||||
}).await;
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::{Query, State, ws::{WebSocket, Message}},
|
extract::{
|
||||||
|
Query, State,
|
||||||
|
ws::{Message, WebSocket},
|
||||||
|
},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
@@ -186,9 +192,11 @@ async fn gate_verify_handler(
|
|||||||
(axum::http::StatusCode::FORBIDDEN, msg).into_response()
|
(axum::http::StatusCode::FORBIDDEN, msg).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => (
|
||||||
(axum::http::StatusCode::NOT_FOUND, "Action not yet authorized (no gate record found).").into_response()
|
axum::http::StatusCode::NOT_FOUND,
|
||||||
}
|
"Action not yet authorized (no gate record found).",
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,18 +237,23 @@ fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
tokio::spawn(reconcile_worker(Arc::clone(&state)));
|
tokio::spawn(reconcile_worker(Arc::clone(&state)));
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
handler: Arc::new(MemoryHandler { state: Arc::clone(&state) }),
|
handler: Arc::new(MemoryHandler {
|
||||||
|
state: Arc::clone(&state),
|
||||||
|
}),
|
||||||
clients: RwLock::new(HashMap::new()),
|
clients: RwLock::new(HashMap::new()),
|
||||||
next_id: AtomicUsize::new(1),
|
next_id: AtomicUsize::new(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/version", get(|| async move {
|
.route(
|
||||||
axum::Json(serde_json::json!({
|
"/api/version",
|
||||||
"version": env!("APP_VERSION"),
|
get(|| async move {
|
||||||
"git_hash": option_env!("GIT_HASH").unwrap_or("unknown")
|
axum::Json(serde_json::json!({
|
||||||
}))
|
"version": env!("APP_VERSION"),
|
||||||
}))
|
"git_hash": option_env!("GIT_HASH").unwrap_or("unknown")
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
)
|
||||||
.route("/ws", get(ws_handler))
|
.route("/ws", get(ws_handler))
|
||||||
.route("/health", get(health_handler))
|
.route("/health", get(health_handler))
|
||||||
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
||||||
@@ -260,65 +273,81 @@ fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
"/",
|
"/",
|
||||||
get(|| async move { axum::response::Html(include_str!("dashboard.html")) }),
|
get(|| async move { axum::response::Html(include_str!("dashboard.html")) }),
|
||||||
)
|
)
|
||||||
.route("/api/graph", get({
|
.route(
|
||||||
let state_clone = app_state.handler.state.clone();
|
"/api/graph",
|
||||||
move || async move {
|
get({
|
||||||
let graph = state_clone.get_full_graph();
|
let state_clone = app_state.handler.state.clone();
|
||||||
axum::Json(graph)
|
move || async move {
|
||||||
}
|
let graph = state_clone.get_full_graph();
|
||||||
}))
|
axum::Json(graph)
|
||||||
|
|
||||||
.route("/api/tasks/{id}/complete", post({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
|
||||||
state_clone.tasks.modify(|tasks| {
|
|
||||||
for t in tasks.iter_mut() {
|
|
||||||
if t.id == id {
|
|
||||||
t.status = "completed".to_string();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
axum::Json(serde_json::json!({"status": "success"}))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
.route("/api/tasks", get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let tasks = state_clone.tasks.read();
|
|
||||||
axum::Json(tasks.clone())
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.route("/api/sticky", get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let sticky = state_clone.sticky.read();
|
|
||||||
axum::Json(sticky.clone())
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.route("/api/search", get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move |axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>| async move {
|
|
||||||
if let Some(q) = params.get("q") {
|
|
||||||
if let Ok(idx) = state_clone.search_index.read() {
|
|
||||||
if let Ok(results) = idx.search(q, None) {
|
|
||||||
let mut formatted_results = Vec::new();
|
|
||||||
for (type_name, content) in results {
|
|
||||||
formatted_results.push(serde_json::json!({
|
|
||||||
"type_name": type_name,
|
|
||||||
"content": content,
|
|
||||||
"score": 1.0
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return axum::Json(serde_json::json!({ "results": formatted_results }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
axum::Json(serde_json::json!({ "results": [] }))
|
}),
|
||||||
}
|
)
|
||||||
}))
|
.route(
|
||||||
.route("/api/stats",
|
"/api/tasks/{id}/complete",
|
||||||
|
post({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||||
|
state_clone.tasks.modify(|tasks| {
|
||||||
|
for t in tasks.iter_mut() {
|
||||||
|
if t.id == id {
|
||||||
|
t.status = "completed".to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
axum::Json(serde_json::json!({"status": "success"}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/tasks",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let tasks = state_clone.tasks.read();
|
||||||
|
axum::Json(tasks.clone())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/sticky",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move || async move {
|
||||||
|
let sticky = state_clone.sticky.read();
|
||||||
|
axum::Json(sticky.clone())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/search",
|
||||||
|
get({
|
||||||
|
let state_clone = app_state.handler.state.clone();
|
||||||
|
move |axum::extract::Query(params): axum::extract::Query<
|
||||||
|
std::collections::HashMap<String, String>,
|
||||||
|
>| async move {
|
||||||
|
if let Some(q) = params.get("q")
|
||||||
|
&& let Ok(idx) = state_clone.search_index.read()
|
||||||
|
&& let Ok(results) = idx.search(q, None) {
|
||||||
|
let mut formatted_results = Vec::new();
|
||||||
|
for (type_name, content) in results {
|
||||||
|
formatted_results.push(serde_json::json!({
|
||||||
|
"type_name": type_name,
|
||||||
|
"content": content,
|
||||||
|
"score": 1.0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return axum::Json(
|
||||||
|
serde_json::json!({ "results": formatted_results }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
axum::Json(serde_json::json!({ "results": [] }))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/stats",
|
||||||
get({
|
get({
|
||||||
let state_clone = app_state.handler.state.clone();
|
let state_clone = app_state.handler.state.clone();
|
||||||
move || async move {
|
move || async move {
|
||||||
@@ -374,7 +403,7 @@ fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
||||||
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr).parse().unwrap();
|
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr).parse().unwrap();
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
|
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
|
||||||
let log_path = dirs::home_dir()
|
let log_path = dirs::home_dir()
|
||||||
@@ -386,29 +415,39 @@ fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async fn ws_handler(
|
async fn ws_handler(
|
||||||
ws: axum::extract::ws::WebSocketUpgrade,
|
ws: axum::extract::ws::WebSocketUpgrade,
|
||||||
headers: axum::http::HeaderMap,
|
_headers: axum::http::HeaderMap,
|
||||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
|
axum::extract::Query(query): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
let client_type = query.get("client").cloned().unwrap_or_else(|| "unknown".to_string());
|
let client_type = query
|
||||||
ws.on_upgrade(move |socket| handle_socket(socket, state, client_type)).into_response()
|
.get("client")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
ws.on_upgrade(move |socket| handle_socket(socket, state, client_type))
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_socket(socket: WebSocket, state: Arc<AppState>, client_type: String) {
|
async fn handle_socket(socket: WebSocket, state: Arc<AppState>, client_type: String) {
|
||||||
let session_id = format!("{}", state.next_id.fetch_add(1, Ordering::SeqCst));
|
let session_id = format!("{}", state.next_id.fetch_add(1, Ordering::SeqCst));
|
||||||
let (tx, mut rx) = mpsc::channel::<String>(100);
|
let (tx, mut rx) = mpsc::channel::<String>(100);
|
||||||
|
|
||||||
state.clients.write().unwrap().insert(session_id.clone(), tx.clone());
|
state
|
||||||
|
.clients
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert(session_id.clone(), tx.clone());
|
||||||
|
|
||||||
let (mut sender, mut receiver) = socket.split();
|
let (mut sender, mut receiver) = socket.split();
|
||||||
|
|
||||||
let mut send_task = tokio::spawn(async move {
|
let mut send_task = tokio::spawn(async move {
|
||||||
while let Some(msg) = rx.recv().await {
|
while let Some(msg) = rx.recv().await {
|
||||||
tracing::trace!("Sending message to websocket (length: {}): {}", msg.len(), msg);
|
tracing::trace!(
|
||||||
|
"Sending message to websocket (length: {}): {}",
|
||||||
|
msg.len(),
|
||||||
|
msg
|
||||||
|
);
|
||||||
if sender.send(Message::Text(msg.into())).await.is_err() {
|
if sender.send(Message::Text(msg.into())).await.is_err() {
|
||||||
tracing::error!("Failed to send message to websocket");
|
tracing::error!("Failed to send message to websocket");
|
||||||
break;
|
break;
|
||||||
@@ -426,75 +465,102 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, client_type: Str
|
|||||||
while let Some(msg_result) = receiver.next().await {
|
while let Some(msg_result) = receiver.next().await {
|
||||||
match msg_result {
|
match msg_result {
|
||||||
Ok(Message::Text(text)) => {
|
Ok(Message::Text(text)) => {
|
||||||
tracing::info!("Received text message from websocket (length: {})", text.len());
|
tracing::info!(
|
||||||
|
"Received text message from websocket (length: {})",
|
||||||
|
text.len()
|
||||||
|
);
|
||||||
tracing::trace!("Message content: {}", text);
|
tracing::trace!("Message content: {}", text);
|
||||||
if let Ok(payload) = serde_json::from_str::<serde_json::Value>(&text) {
|
if let Ok(payload) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
if client_type == "proxy" {
|
if client_type == "proxy" {
|
||||||
// Send activity broadcast to UI clients
|
// Send activity broadcast to UI clients
|
||||||
if let Some(method) = payload.get("method").and_then(|m| m.as_str()) {
|
if let Some(method) = payload.get("method").and_then(|m| m.as_str())
|
||||||
if method == "tools/call" {
|
&& method == "tools/call" {
|
||||||
let name = payload.get("params").and_then(|p| p.get("name")).and_then(|n| n.as_str()).unwrap_or("unknown_tool");
|
let name = payload
|
||||||
let activity_msg = format!("Agent executed tool: {}", name);
|
.get("params")
|
||||||
|
.and_then(|p| p.get("name"))
|
||||||
let event = serde_json::json!({
|
.and_then(|n| n.as_str())
|
||||||
"type": "activity",
|
.unwrap_or("unknown_tool");
|
||||||
"data": activity_msg
|
let activity_msg = format!("Agent executed tool: {}", name);
|
||||||
});
|
|
||||||
|
|
||||||
let clients_map = state_clone.clients.read().unwrap().clone();
|
|
||||||
for (id, client_tx) in clients_map.iter() {
|
|
||||||
if id != &session_id_clone {
|
|
||||||
let _ = client_tx.send(event.to_string()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // End if proxy
|
|
||||||
|
|
||||||
// Process MCP request
|
|
||||||
if let Some(response) = handler.handle_request(payload).await {
|
|
||||||
let res_str = serde_json::to_string(&response).unwrap();
|
|
||||||
let tx_opt = state_clone.clients.read().unwrap().get(&session_id_clone).cloned();
|
|
||||||
if let Some(client_tx) = tx_opt {
|
|
||||||
if let Err(e) = client_tx.send(res_str).await {
|
|
||||||
tracing::error!("Failed to send response to client channel for session {}: {}", session_id_clone, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::warn!("Could not find client_tx for session_id {} when trying to send response", session_id_clone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // End if let Ok(payload)
|
|
||||||
else {
|
|
||||||
tracing::warn!("Failed to parse payload as JSON from websocket message: {}", text);
|
|
||||||
}
|
|
||||||
} // End Ok(Message::Text(text))
|
|
||||||
Ok(other) => {
|
|
||||||
tracing::info!("Received non-text message from websocket: {:?}", other);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Websocket receive error: {}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracing::info!("Websocket receiver task ended for session {}", session_id_clone);
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = (&mut send_task) => {
|
|
||||||
tracing::info!("Websocket send task finished for session {}", session_id);
|
|
||||||
recv_task.abort();
|
|
||||||
},
|
|
||||||
_ = (&mut recv_task) => {
|
|
||||||
tracing::info!("Websocket recv task finished for session {}", session_id);
|
|
||||||
send_task.abort();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
state.clients.write().unwrap().remove(&session_id);
|
|
||||||
tracing::info!("Websocket session {} closed and removed from state", session_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let event = serde_json::json!({
|
||||||
|
"type": "activity",
|
||||||
|
"data": activity_msg
|
||||||
|
});
|
||||||
|
|
||||||
|
let clients_map = state_clone.clients.read().unwrap().clone();
|
||||||
|
for (id, client_tx) in clients_map.iter() {
|
||||||
|
if id != &session_id_clone {
|
||||||
|
let _ = client_tx.send(event.to_string()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // End if proxy
|
||||||
|
|
||||||
|
// Process MCP request
|
||||||
|
if let Some(response) = handler.handle_request(payload).await {
|
||||||
|
let res_str = serde_json::to_string(&response).unwrap();
|
||||||
|
let tx_opt = state_clone
|
||||||
|
.clients
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&session_id_clone)
|
||||||
|
.cloned();
|
||||||
|
if let Some(client_tx) = tx_opt {
|
||||||
|
if let Err(e) = client_tx.send(res_str).await {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to send response to client channel for session {}: {}",
|
||||||
|
session_id_clone,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
"Could not find client_tx for session_id {} when trying to send response",
|
||||||
|
session_id_clone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End if let Ok(payload)
|
||||||
|
else {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to parse payload as JSON from websocket message: {}",
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} // End Ok(Message::Text(text))
|
||||||
|
Ok(other) => {
|
||||||
|
tracing::info!("Received non-text message from websocket: {:?}", other);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Websocket receive error: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!(
|
||||||
|
"Websocket receiver task ended for session {}",
|
||||||
|
session_id_clone
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = (&mut send_task) => {
|
||||||
|
tracing::info!("Websocket send task finished for session {}", session_id);
|
||||||
|
recv_task.abort();
|
||||||
|
},
|
||||||
|
_ = (&mut recv_task) => {
|
||||||
|
tracing::info!("Websocket recv task finished for session {}", session_id);
|
||||||
|
send_task.abort();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
state.clients.write().unwrap().remove(&session_id);
|
||||||
|
tracing::info!(
|
||||||
|
"Websocket session {} closed and removed from state",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||||
pub struct NvimTelemetry {
|
pub struct NvimTelemetry {
|
||||||
@@ -510,11 +576,13 @@ async fn nvim_telemetry_handler(
|
|||||||
axum::Json(payload): axum::Json<NvimTelemetry>,
|
axum::Json(payload): axum::Json<NvimTelemetry>,
|
||||||
) -> impl axum::response::IntoResponse {
|
) -> impl axum::response::IntoResponse {
|
||||||
// 1. Update active_nvim.txt if FocusGained, BufEnter, or VimEnter
|
// 1. Update active_nvim.txt if FocusGained, BufEnter, or VimEnter
|
||||||
if payload.event == "FocusGained" || payload.event == "BufEnter" || payload.event == "VimEnter" {
|
if payload.event == "FocusGained" || payload.event == "BufEnter" || payload.event == "VimEnter"
|
||||||
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 win_path = format!("{}\\.gemini\\active_nvim.txt", profile);
|
let win_path = format!("{}\\.gemini\\active_nvim.txt", profile);
|
||||||
let _ = std::fs::write(&win_path, &payload.session_id);
|
let _ = std::fs::write(&win_path, &payload.session_id);
|
||||||
|
|
||||||
let wsl_path = "\\\\wsl.localhost\\Ubuntu\\home\\riz\\.gemini\\active_nvim.txt";
|
let wsl_path = "\\\\wsl.localhost\\Ubuntu\\home\\riz\\.gemini\\active_nvim.txt";
|
||||||
let _ = std::fs::write(wsl_path, &payload.session_id);
|
let _ = std::fs::write(wsl_path, &payload.session_id);
|
||||||
}
|
}
|
||||||
@@ -524,10 +592,10 @@ async fn nvim_telemetry_handler(
|
|||||||
"type": "nvim_telemetry",
|
"type": "nvim_telemetry",
|
||||||
"data": payload
|
"data": payload
|
||||||
});
|
});
|
||||||
|
|
||||||
let msg_str = ws_msg.to_string();
|
let msg_str = ws_msg.to_string();
|
||||||
let clients = state.clients.read().unwrap().clone();
|
let clients = state.clients.read().unwrap().clone();
|
||||||
for (_, tx) in clients.iter() {
|
for tx in clients.values() {
|
||||||
let _ = tx.send(msg_str.clone()).await;
|
let _ = tx.send(msg_str.clone()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,10 +617,10 @@ fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::Worker
|
|||||||
});
|
});
|
||||||
let log_dir = std::path::PathBuf::from(base_dir).join("logs");
|
let log_dir = std::path::PathBuf::from(base_dir).join("logs");
|
||||||
std::fs::create_dir_all(&log_dir).unwrap_or_default();
|
std::fs::create_dir_all(&log_dir).unwrap_or_default();
|
||||||
|
|
||||||
let file_appender = tracing_appender::rolling::daily(log_dir, format!("{}.log", app_name));
|
let file_appender = tracing_appender::rolling::daily(log_dir, format!("{}.log", app_name));
|
||||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
|
||||||
let _ = tracing_subscriber::fmt()
|
let _ = tracing_subscriber::fmt()
|
||||||
.with_writer(non_blocking)
|
.with_writer(non_blocking)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
@@ -560,7 +628,7 @@ fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::Worker
|
|||||||
.with_thread_ids(true)
|
.with_thread_ids(true)
|
||||||
.with_thread_names(true)
|
.with_thread_names(true)
|
||||||
.try_init();
|
.try_init();
|
||||||
|
|
||||||
Some(guard)
|
Some(guard)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,96 +679,92 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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().into_owned()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| ".gemini/mcp_memory".into())
|
||||||
|
});
|
||||||
|
let base = PathBuf::from(base_dir);
|
||||||
|
fs::create_dir_all(&base).expect("Failed to create store dir");
|
||||||
|
|
||||||
let base_dir = std::env::var("MCP_MEMORY_STORE_DIR").unwrap_or_else(|_| {
|
let redb_path = base.join("mcp_store.redb");
|
||||||
dirs::home_dir()
|
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
|
||||||
.map(|mut h| {
|
|
||||||
h.push(".gemini/mcp_memory");
|
|
||||||
h.to_string_lossy().into_owned()
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| ".gemini/mcp_memory".into())
|
|
||||||
});
|
|
||||||
let base = PathBuf::from(base_dir);
|
|
||||||
fs::create_dir_all(&base).expect("Failed to create store dir");
|
|
||||||
|
|
||||||
let redb_path = base.join("mcp_store.redb");
|
// Ensure table exists and migrate old JSON files
|
||||||
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
|
{
|
||||||
|
let write_txn = db.begin_write().unwrap();
|
||||||
// Ensure table exists and migrate old JSON files
|
|
||||||
{
|
{
|
||||||
let write_txn = db.begin_write().unwrap();
|
let mut table = write_txn.open_table(crate::store::STORE_TABLE).unwrap();
|
||||||
{
|
|
||||||
let mut table = write_txn.open_table(crate::store::STORE_TABLE).unwrap();
|
|
||||||
|
|
||||||
let stores = vec![
|
|
||||||
("knowledge_graph_master", "knowledge_graph_master.json"),
|
|
||||||
("audit_ledger", "audit_ledger.json"),
|
|
||||||
("sticky_notes", "sticky_notes.json"),
|
|
||||||
("tasks", "tasks.json"),
|
|
||||||
("snippets", "snippets.json"),
|
|
||||||
("adrs", "adrs.json"),
|
|
||||||
("preferences", "preferences.json"),
|
|
||||||
("error_fixes", "error_fixes.json"),
|
|
||||||
("pinned_files", "pinned_files.json"),
|
|
||||||
("session_summaries", "session_summaries.json"),
|
|
||||||
("handoff_memos", "handoff_memos.json"),
|
|
||||||
("env_fingerprints", "env_fingerprints.json"),
|
|
||||||
("env_requirements", "env_requirements.json"),
|
|
||||||
("milestones", "milestones.json"),
|
|
||||||
("environments", "environments.json"),
|
|
||||||
("pr_checklists", "pr_checklists.json"),
|
|
||||||
("tech_debts", "tech_debts.json"),
|
|
||||||
("gates", "gates.json"),
|
|
||||||
("context_workspaces", "context_workspaces.json"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (key, file_name) in stores.iter() {
|
let stores = vec![
|
||||||
if table.get(*key).unwrap().is_none() {
|
("knowledge_graph_master", "knowledge_graph_master.json"),
|
||||||
let json_path = base.join(file_name);
|
("audit_ledger", "audit_ledger.json"),
|
||||||
if json_path.exists() {
|
("sticky_notes", "sticky_notes.json"),
|
||||||
if let Ok(data) = fs::read(&json_path) {
|
("tasks", "tasks.json"),
|
||||||
if serde_json::from_slice::<serde_json::Value>(&data).is_ok() {
|
("snippets", "snippets.json"),
|
||||||
table.insert(*key, data.as_slice()).unwrap();
|
("adrs", "adrs.json"),
|
||||||
let _ = fs::rename(&json_path, json_path.with_extension("json.migrated"));
|
("preferences", "preferences.json"),
|
||||||
}
|
("error_fixes", "error_fixes.json"),
|
||||||
|
("pinned_files", "pinned_files.json"),
|
||||||
|
("session_summaries", "session_summaries.json"),
|
||||||
|
("handoff_memos", "handoff_memos.json"),
|
||||||
|
("env_fingerprints", "env_fingerprints.json"),
|
||||||
|
("env_requirements", "env_requirements.json"),
|
||||||
|
("milestones", "milestones.json"),
|
||||||
|
("environments", "environments.json"),
|
||||||
|
("pr_checklists", "pr_checklists.json"),
|
||||||
|
("tech_debts", "tech_debts.json"),
|
||||||
|
("gates", "gates.json"),
|
||||||
|
("context_workspaces", "context_workspaces.json"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (key, file_name) in stores.iter() {
|
||||||
|
if table.get(*key).unwrap().is_none() {
|
||||||
|
let json_path = base.join(file_name);
|
||||||
|
if json_path.exists()
|
||||||
|
&& let Ok(data) = fs::read(&json_path)
|
||||||
|
&& serde_json::from_slice::<serde_json::Value>(&data).is_ok() {
|
||||||
|
table.insert(*key, data.as_slice()).unwrap();
|
||||||
|
let _ = fs::rename(
|
||||||
|
&json_path,
|
||||||
|
json_path.with_extension("json.migrated"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
write_txn.commit().unwrap();
|
|
||||||
}
|
}
|
||||||
|
write_txn.commit().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let state = Arc::new(MemoryState {
|
let state = Arc::new(MemoryState {
|
||||||
graph: Store::new("knowledge_graph_master", db.clone()),
|
graph: Store::new("knowledge_graph_master", db.clone()),
|
||||||
base_dir: base.clone(),
|
base_dir: base.clone(),
|
||||||
search_index: RwLock::new(crate::search::MemoryIndex::new(&base).unwrap()),
|
search_index: RwLock::new(crate::search::MemoryIndex::new(&base).unwrap()),
|
||||||
ledger: Store::new("audit_ledger", db.clone()),
|
ledger: Store::new("audit_ledger", db.clone()),
|
||||||
sticky: Store::new("sticky_notes", db.clone()),
|
sticky: Store::new("sticky_notes", db.clone()),
|
||||||
tasks: Store::new("tasks", db.clone()),
|
tasks: Store::new("tasks", db.clone()),
|
||||||
snippets: Store::new("snippets", db.clone()),
|
snippets: Store::new("snippets", db.clone()),
|
||||||
adrs: Store::new("adrs", db.clone()),
|
adrs: Store::new("adrs", db.clone()),
|
||||||
prefs: Store::new("preferences", db.clone()),
|
prefs: Store::new("preferences", db.clone()),
|
||||||
error_fixes: Store::new("error_fixes", db.clone()),
|
error_fixes: Store::new("error_fixes", db.clone()),
|
||||||
pinned_files: Store::new("pinned_files", db.clone()),
|
pinned_files: Store::new("pinned_files", db.clone()),
|
||||||
session_summaries: Store::new("session_summaries", db.clone()),
|
session_summaries: Store::new("session_summaries", db.clone()),
|
||||||
handoff_memos: Store::new("handoff_memos", db.clone()),
|
handoff_memos: Store::new("handoff_memos", db.clone()),
|
||||||
env_fingerprints: Store::new("env_fingerprints", db.clone()),
|
env_fingerprints: Store::new("env_fingerprints", db.clone()),
|
||||||
env_requirements: Store::new("env_requirements", db.clone()),
|
env_requirements: Store::new("env_requirements", db.clone()),
|
||||||
milestones: Store::new("milestones", db.clone()),
|
milestones: Store::new("milestones", db.clone()),
|
||||||
environments: Store::new("environments", db.clone()),
|
environments: Store::new("environments", db.clone()),
|
||||||
pr_checklists: Store::new("pr_checklists", db.clone()),
|
pr_checklists: Store::new("pr_checklists", db.clone()),
|
||||||
tech_debts: Store::new("tech_debts", db.clone()),
|
tech_debts: Store::new("tech_debts", db.clone()),
|
||||||
gates: Store::new("gates", db.clone()),
|
gates: Store::new("gates", db.clone()),
|
||||||
context_workspaces: Store::new("context_workspaces", db.clone()),
|
context_workspaces: Store::new("context_workspaces", db.clone()),
|
||||||
activity_tx: tokio::sync::broadcast::channel(100).0,
|
activity_tx: tokio::sync::broadcast::channel(100).0,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.rebuild_index();
|
state.rebuild_index();
|
||||||
|
|
||||||
run_server(state)
|
run_server(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use schemars::JsonSchema;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct CodeChange {
|
pub struct CodeChange {
|
||||||
|
|||||||
+13
-8
@@ -28,7 +28,8 @@ impl MemoryIndex {
|
|||||||
|
|
||||||
let index_dir = store_dir.join("tantivy_index");
|
let index_dir = store_dir.join("tantivy_index");
|
||||||
std::fs::create_dir_all(&index_dir).unwrap();
|
std::fs::create_dir_all(&index_dir).unwrap();
|
||||||
let index = Index::open_in_dir(&index_dir).unwrap_or_else(|_| Index::create_in_dir(&index_dir, schema.clone()).unwrap());
|
let index = Index::open_in_dir(&index_dir)
|
||||||
|
.unwrap_or_else(|_| Index::create_in_dir(&index_dir, schema.clone()).unwrap());
|
||||||
|
|
||||||
let writer = index.writer(50_000_000)?;
|
let writer = index.writer(50_000_000)?;
|
||||||
let reader = index
|
let reader = index
|
||||||
@@ -72,7 +73,6 @@ impl MemoryIndex {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn commit(&self) -> tantivy::Result<()> {
|
pub fn commit(&self) -> tantivy::Result<()> {
|
||||||
let mut writer = self.writer.lock().unwrap();
|
let mut writer = self.writer.lock().unwrap();
|
||||||
writer.commit()?;
|
writer.commit()?;
|
||||||
@@ -113,9 +113,11 @@ impl MemoryIndex {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
if let Some(ns) = namespace
|
if let Some(ns) = namespace
|
||||||
&& doc_ns != ns && doc_ns != "global" {
|
&& doc_ns != ns
|
||||||
continue;
|
&& doc_ns != "global"
|
||||||
}
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
results.push((id, doc_type));
|
results.push((id, doc_type));
|
||||||
}
|
}
|
||||||
Ok(results)
|
Ok(results)
|
||||||
@@ -172,7 +174,10 @@ mod tests {
|
|||||||
status: "open".to_string(),
|
status: "open".to_string(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
updated_at: 0,
|
updated_at: 0,
|
||||||
git_branch: None, acceptance_criteria: vec![], dependencies: vec![], parent_id: None,
|
git_branch: None,
|
||||||
|
acceptance_criteria: vec![],
|
||||||
|
dependencies: vec![],
|
||||||
|
parent_id: None,
|
||||||
};
|
};
|
||||||
index.index_task(&task).unwrap();
|
index.index_task(&task).unwrap();
|
||||||
|
|
||||||
@@ -218,11 +223,11 @@ mod tests {
|
|||||||
fn test_search_malformed_query() {
|
fn test_search_malformed_query() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let index = MemoryIndex::new(temp_dir.path()).unwrap();
|
let index = MemoryIndex::new(temp_dir.path()).unwrap();
|
||||||
|
|
||||||
// Malformed lucene query (unclosed parenthesis)
|
// Malformed lucene query (unclosed parenthesis)
|
||||||
let result = index.search("title: (unclosed", None);
|
let result = index.search("title: (unclosed", None);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
// Another malformed query (unclosed quote)
|
// Another malformed query (unclosed quote)
|
||||||
let result2 = index.search("title: \"unclosed", None);
|
let result2 = index.search("title: \"unclosed", None);
|
||||||
assert!(result2.is_err());
|
assert!(result2.is_err());
|
||||||
|
|||||||
+8
-6
@@ -33,19 +33,21 @@ pub struct MemoryState {
|
|||||||
impl MemoryState {
|
impl MemoryState {
|
||||||
pub fn unique_items<T: Eq + std::hash::Hash + Clone>(input: Vec<T>) -> Vec<T> {
|
pub fn unique_items<T: Eq + std::hash::Hash + Clone>(input: Vec<T>) -> Vec<T> {
|
||||||
let mut keys = std::collections::HashSet::new();
|
let mut keys = std::collections::HashSet::new();
|
||||||
input.into_iter().filter(|entry| keys.insert(entry.clone())).collect()
|
input
|
||||||
|
.into_iter()
|
||||||
|
.filter(|entry| keys.insert(entry.clone()))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn broadcast_activity(&self, message: &str) {
|
pub fn broadcast_activity(&self, message: &str) {
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
"type": "activity",
|
"type": "activity",
|
||||||
"data": message
|
"data": message
|
||||||
}).to_string();
|
})
|
||||||
|
.to_string();
|
||||||
let _ = self.activity_tx.send(payload);
|
let _ = self.activity_tx.send(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_full_graph(&self) -> KnowledgeGraph {
|
pub fn get_full_graph(&self) -> KnowledgeGraph {
|
||||||
self.graph.read()
|
self.graph.read()
|
||||||
}
|
}
|
||||||
@@ -61,10 +63,10 @@ impl MemoryState {
|
|||||||
pub fn rebuild_index(&self) {
|
pub fn rebuild_index(&self) {
|
||||||
if let Ok(new_idx) = MemoryIndex::new(&self.base_dir) {
|
if let Ok(new_idx) = MemoryIndex::new(&self.base_dir) {
|
||||||
let graph = self.graph.read();
|
let graph = self.graph.read();
|
||||||
for (_, e) in &graph.entities {
|
for e in graph.entities.values() {
|
||||||
let _ = new_idx.index_entity(e);
|
let _ = new_idx.index_entity(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tasks = self.tasks.read();
|
let tasks = self.tasks.read();
|
||||||
for t in tasks {
|
for t in tasks {
|
||||||
let _ = new_idx.index_task(&t);
|
let _ = new_idx.index_task(&t);
|
||||||
|
|||||||
+23
-13
@@ -22,13 +22,11 @@ impl<T: DeserializeOwned + Default + Serialize + Clone + Send + 'static> Store<T
|
|||||||
|
|
||||||
fn load_from_db(key: &str, db: &Database) -> T {
|
fn load_from_db(key: &str, db: &Database) -> T {
|
||||||
let read_txn = db.begin_read().unwrap();
|
let read_txn = db.begin_read().unwrap();
|
||||||
if let Ok(table) = read_txn.open_table(STORE_TABLE) {
|
if let Ok(table) = read_txn.open_table(STORE_TABLE)
|
||||||
if let Ok(Some(value)) = table.get(key) {
|
&& let Ok(Some(value)) = table.get(key)
|
||||||
if let Ok(parsed) = serde_json::from_slice::<T>(value.value()) {
|
&& let Ok(parsed) = serde_json::from_slice::<T>(value.value()) {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
T::default()
|
T::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ mod tests {
|
|||||||
async fn test_store_read_write() {
|
async fn test_store_read_write() {
|
||||||
let temp_file = NamedTempFile::new().unwrap();
|
let temp_file = NamedTempFile::new().unwrap();
|
||||||
let db = Database::create(temp_file.path()).unwrap();
|
let db = Database::create(temp_file.path()).unwrap();
|
||||||
|
|
||||||
let write_txn = db.begin_write().unwrap();
|
let write_txn = db.begin_write().unwrap();
|
||||||
{
|
{
|
||||||
write_txn.open_table(STORE_TABLE).unwrap();
|
write_txn.open_table(STORE_TABLE).unwrap();
|
||||||
@@ -94,18 +92,30 @@ mod tests {
|
|||||||
// Need to wait for spawn_blocking to finish
|
// Need to wait for spawn_blocking to finish
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
|
||||||
assert_eq!(store.read(), TestData { name: "Hello".to_string(), value: 42 });
|
assert_eq!(
|
||||||
|
store.read(),
|
||||||
|
TestData {
|
||||||
|
name: "Hello".to_string(),
|
||||||
|
value: 42
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Load again to verify persistence
|
// Load again to verify persistence
|
||||||
let store2 = Store::<TestData>::new("test_key", db.clone());
|
let store2 = Store::<TestData>::new("test_key", db.clone());
|
||||||
assert_eq!(store2.read(), TestData { name: "Hello".to_string(), value: 42 });
|
assert_eq!(
|
||||||
|
store2.read(),
|
||||||
|
TestData {
|
||||||
|
name: "Hello".to_string(),
|
||||||
|
value: 42
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
async fn test_store_concurrency() {
|
async fn test_store_concurrency() {
|
||||||
let temp_file = NamedTempFile::new().unwrap();
|
let temp_file = NamedTempFile::new().unwrap();
|
||||||
let db = Database::create(temp_file.path()).unwrap();
|
let db = Database::create(temp_file.path()).unwrap();
|
||||||
|
|
||||||
let write_txn = db.begin_write().unwrap();
|
let write_txn = db.begin_write().unwrap();
|
||||||
{
|
{
|
||||||
write_txn.open_table(STORE_TABLE).unwrap();
|
write_txn.open_table(STORE_TABLE).unwrap();
|
||||||
@@ -114,7 +124,7 @@ mod tests {
|
|||||||
|
|
||||||
let db = Arc::new(db);
|
let db = Arc::new(db);
|
||||||
let store = Arc::new(Store::<TestData>::new("concurrent_key", db.clone()));
|
let store = Arc::new(Store::<TestData>::new("concurrent_key", db.clone()));
|
||||||
|
|
||||||
let mut handles = vec![];
|
let mut handles = vec![];
|
||||||
for _ in 0..50 {
|
for _ in 0..50 {
|
||||||
let s = store.clone();
|
let s = store.clone();
|
||||||
@@ -124,14 +134,14 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
for h in handles {
|
for h in handles {
|
||||||
h.await.unwrap();
|
h.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all blocking writes to flush
|
// Wait for all blocking writes to flush
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
assert_eq!(store.read().value, 50);
|
assert_eq!(store.read().value, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+29
-14
@@ -3,53 +3,68 @@ use std::collections::HashSet;
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_eager_tools_parity() {
|
fn test_eager_tools_parity() {
|
||||||
// 1. Read handlers.rs to get memory tools
|
// 1. Read handlers.rs to get memory tools
|
||||||
let memory_source = std::fs::read_to_string("src/handlers.rs").expect("Failed to read handlers.rs");
|
let memory_source =
|
||||||
|
std::fs::read_to_string("src/handlers.rs").expect("Failed to read handlers.rs");
|
||||||
let mut memory_tools = HashSet::new();
|
let mut memory_tools = HashSet::new();
|
||||||
for line in memory_source.lines() {
|
for line in memory_source.lines() {
|
||||||
if line.contains("crate::mcp::tool_def") {
|
if line.contains("crate::mcp::tool_def")
|
||||||
if let Some(start) = line.find("(\"") {
|
&& let Some(start) = line.find("(\"") {
|
||||||
let rest = &line[start + 2..];
|
let rest = &line[start + 2..];
|
||||||
if let Some(end) = rest.find("\"") {
|
if let Some(end) = rest.find("\"") {
|
||||||
memory_tools.insert(rest[..end].to_string());
|
memory_tools.insert(rest[..end].to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
assert!(!memory_tools.is_empty(), "Could not find memory tools in handlers.rs");
|
assert!(
|
||||||
|
!memory_tools.is_empty(),
|
||||||
|
"Could not find memory tools in handlers.rs"
|
||||||
|
);
|
||||||
|
|
||||||
// 2. Read nvim-core/src/lib.rs to get nvim tools
|
// 2. Read nvim-core/src/lib.rs to get nvim tools
|
||||||
let nvim_source = std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs");
|
let nvim_source =
|
||||||
|
std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs");
|
||||||
let mut nvim_tools = HashSet::new();
|
let mut nvim_tools = HashSet::new();
|
||||||
for line in nvim_source.lines() {
|
for line in nvim_source.lines() {
|
||||||
if line.contains("\"name\": \"nvim_") {
|
if line.contains("\"name\": \"nvim_")
|
||||||
if let Some(start) = line.find("\"name\": \"") {
|
&& let Some(start) = line.find("\"name\": \"") {
|
||||||
let rest = &line[start + 9..];
|
let rest = &line[start + 9..];
|
||||||
if let Some(end) = rest.find("\"") {
|
if let Some(end) = rest.find("\"") {
|
||||||
nvim_tools.insert(rest[..end].to_string());
|
nvim_tools.insert(rest[..end].to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
assert!(!nvim_tools.is_empty(), "Could not find nvim tools in lib.rs");
|
assert!(
|
||||||
|
!nvim_tools.is_empty(),
|
||||||
|
"Could not find nvim tools in lib.rs"
|
||||||
|
);
|
||||||
|
|
||||||
// 3. Read Windows mcp_config.json
|
// 3. Read Windows mcp_config.json
|
||||||
let win_home = std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".to_string());
|
let win_home =
|
||||||
|
std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".to_string());
|
||||||
let win_config_path = std::path::PathBuf::from(win_home).join(".gemini/config/mcp_config.json");
|
let win_config_path = std::path::PathBuf::from(win_home).join(".gemini/config/mcp_config.json");
|
||||||
if win_config_path.exists() {
|
if win_config_path.exists() {
|
||||||
let config_str = std::fs::read_to_string(&win_config_path).unwrap();
|
let config_str = std::fs::read_to_string(&win_config_path).unwrap();
|
||||||
let config: serde_json::Value = serde_json::from_str(&config_str).unwrap();
|
let config: serde_json::Value = serde_json::from_str(&config_str).unwrap();
|
||||||
|
|
||||||
if let Some(eager) = config["mcpServers"]["memory"]["eagerTools"].as_array() {
|
if let Some(eager) = config["mcpServers"]["memory"]["eagerTools"].as_array() {
|
||||||
for tool in eager {
|
for tool in eager {
|
||||||
let name = tool.as_str().unwrap();
|
let name = tool.as_str().unwrap();
|
||||||
assert!(memory_tools.contains(name), "Windows config Memory tool '{}' not implemented in handlers.rs!", name);
|
assert!(
|
||||||
|
memory_tools.contains(name),
|
||||||
|
"Windows config Memory tool '{}' not implemented in handlers.rs!",
|
||||||
|
name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(nvim_eager) = config["mcpServers"]["win-nvim"]["eagerTools"].as_array() {
|
if let Some(nvim_eager) = config["mcpServers"]["win-nvim"]["eagerTools"].as_array() {
|
||||||
for tool in nvim_eager {
|
for tool in nvim_eager {
|
||||||
let name = tool.as_str().unwrap();
|
let name = tool.as_str().unwrap();
|
||||||
assert!(nvim_tools.contains(name), "Windows config Nvim tool '{}' not implemented in nvim-core!", name);
|
assert!(
|
||||||
|
nvim_tools.contains(name),
|
||||||
|
"Windows config Nvim tool '{}' not implemented in nvim-core!",
|
||||||
|
name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-9
@@ -2,19 +2,24 @@ use std::process::Command;
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let git_hash = Command::new("git")
|
let git_hash = Command::new("git")
|
||||||
.args(&["rev-parse", "--short", "HEAD"])
|
.args(["rev-parse", "--short", "HEAD"])
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
|
|
||||||
let git_date = Command::new("git")
|
|
||||||
.args(&["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
let version = format!("{} ({} {})", env!("CARGO_PKG_VERSION"), git_date.trim(), git_hash.trim());
|
let git_date = Command::new("git")
|
||||||
|
.args(["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
let version = format!(
|
||||||
|
"{} ({} {})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
git_date.trim(),
|
||||||
|
git_hash.trim()
|
||||||
|
);
|
||||||
println!("cargo:rustc-env=APP_VERSION={}", version);
|
println!("cargo:rustc-env=APP_VERSION={}", version);
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
|
use futures_util::StreamExt;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use futures_util::StreamExt;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let target = env::var("MCP_TARGET").unwrap_or_else(|_| "https://127.0.0.1:3000".to_string());
|
let target = env::var("MCP_TARGET").unwrap_or_else(|_| "https://127.0.0.1:3000".to_string());
|
||||||
let token = env::var("MCP_AUTH_TOKEN").unwrap_or_else(|_| "jP76lUJ5DtFRZmcvXH8LKdCTIkp29eAf".to_string());
|
let token = env::var("MCP_AUTH_TOKEN")
|
||||||
|
.unwrap_or_else(|_| "jP76lUJ5DtFRZmcvXH8LKdCTIkp29eAf".to_string());
|
||||||
|
|
||||||
tracing::info!("Starting skeletal client to {}", target);
|
tracing::info!("Starting skeletal client to {}", target);
|
||||||
|
|
||||||
@@ -17,13 +18,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let sse_url = format!("{}/sse", target);
|
let sse_url = format!("{}/sse", target);
|
||||||
|
|
||||||
tracing::info!("Connecting to SSE: {}", sse_url);
|
tracing::info!("Connecting to SSE: {}", sse_url);
|
||||||
|
|
||||||
let res = client.get(&sse_url)
|
let res = client.get(&sse_url).bearer_auth(&token).send().await?;
|
||||||
.bearer_auth(&token)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
tracing::error!("Failed to connect to SSE: {}", res.status());
|
tracing::error!("Failed to connect to SSE: {}", res.status());
|
||||||
@@ -40,15 +38,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
while let Some(chunk) = stream.next().await {
|
while let Some(chunk) = stream.next().await {
|
||||||
let bytes = chunk?;
|
let bytes = chunk?;
|
||||||
buffer.extend_from_slice(&bytes);
|
buffer.extend_from_slice(&bytes);
|
||||||
|
|
||||||
while let Some(pos) = buffer.windows(2).position(|w| w == b"\n\n" || w == b"\r\n") {
|
while let Some(pos) = buffer.windows(2).position(|w| w == b"\n\n" || w == b"\r\n") {
|
||||||
let msg_bytes = buffer.drain(..pos).collect::<Vec<_>>();
|
let msg_bytes = buffer.drain(..pos).collect::<Vec<_>>();
|
||||||
buffer.drain(..2);
|
buffer.drain(..2);
|
||||||
|
|
||||||
let text = String::from_utf8_lossy(&msg_bytes);
|
let text = String::from_utf8_lossy(&msg_bytes);
|
||||||
let mut is_endpoint = false;
|
let mut is_endpoint = false;
|
||||||
let mut data_content = String::new();
|
let mut data_content = String::new();
|
||||||
|
|
||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
if line.starts_with("event: endpoint") {
|
if line.starts_with("event: endpoint") {
|
||||||
is_endpoint = true;
|
is_endpoint = true;
|
||||||
@@ -78,7 +76,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
tracing::info!("Sending test payload to {}", post_url);
|
tracing::info!("Sending test payload to {}", post_url);
|
||||||
tracing::info!("Payload: {}", payload);
|
tracing::info!("Payload: {}", payload);
|
||||||
|
|
||||||
let post_res = client.post(&post_url)
|
let post_res = client
|
||||||
|
.post(&post_url)
|
||||||
.bearer_auth(&token)
|
.bearer_auth(&token)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(payload.to_string())
|
.body(payload.to_string())
|
||||||
@@ -91,7 +90,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Wait for the SSE stream to deliver the response
|
// Wait for the SSE stream to deliver the response
|
||||||
tracing::info!("Waiting 2 seconds for SSE response delivery...");
|
tracing::info!("Waiting 2 seconds for SSE response delivery...");
|
||||||
|
|
||||||
let mut timeout = tokio::time::interval(Duration::from_secs(2));
|
let mut timeout = tokio::time::interval(Duration::from_secs(2));
|
||||||
timeout.tick().await; // first tick is immediate
|
timeout.tick().await; // first tick is immediate
|
||||||
|
|
||||||
|
|||||||
+7
-9
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::io::AsyncBufReadExt;
|
use tokio::io::AsyncBufReadExt;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@@ -23,11 +23,11 @@ async fn read_mcp_message(stdin: &mut tokio::io::BufReader<tokio::io::Stdin>) ->
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
tracing::info!("Read {} bytes from stdin: {:?}", bytes_read, line);
|
tracing::info!("Read {} bytes from stdin: {:?}", bytes_read, line);
|
||||||
|
|
||||||
if line.starts_with('{') {
|
if line.starts_with('{') {
|
||||||
return Some(line.trim_end().to_string());
|
return Some(line.trim_end().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = line.trim_end();
|
let line = line.trim_end();
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
break;
|
break;
|
||||||
@@ -51,16 +51,16 @@ fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::Worker
|
|||||||
let mut base_dir = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
|
let mut base_dir = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||||
base_dir.push(".gemini/mcp_memory/logs");
|
base_dir.push(".gemini/mcp_memory/logs");
|
||||||
std::fs::create_dir_all(&base_dir).unwrap_or_default();
|
std::fs::create_dir_all(&base_dir).unwrap_or_default();
|
||||||
|
|
||||||
let file_appender = tracing_appender::rolling::daily(base_dir, format!("{}.log", app_name));
|
let file_appender = tracing_appender::rolling::daily(base_dir, format!("{}.log", app_name));
|
||||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
|
||||||
let _ = tracing_subscriber::fmt()
|
let _ = tracing_subscriber::fmt()
|
||||||
.with_writer(non_blocking)
|
.with_writer(non_blocking)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.with_max_level(tracing::Level::TRACE)
|
.with_max_level(tracing::Level::TRACE)
|
||||||
.try_init();
|
.try_init();
|
||||||
|
|
||||||
Some(guard)
|
Some(guard)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
tracing::info!("Attempting to connect to {}", ws_url);
|
tracing::info!("Attempting to connect to {}", ws_url);
|
||||||
|
|
||||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
let mut request = match ws_url.clone().into_client_request() {
|
let request = match ws_url.clone().into_client_request() {
|
||||||
Ok(req) => req,
|
Ok(req) => req,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to parse target URL {}: {}", ws_url, e);
|
tracing::error!("Failed to parse target URL {}: {}", ws_url, e);
|
||||||
@@ -165,5 +165,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
+40
-25
@@ -1,5 +1,5 @@
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{Value, json};
|
||||||
use std::io::{BufRead, BufReader, Read, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -18,21 +18,26 @@ fn read_message(reader: &mut impl BufRead) -> Option<Value> {
|
|||||||
serde_json::from_str(line.trim()).ok()
|
serde_json::from_str(line.trim()).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_full_system_e2e_performance() {
|
async fn test_full_system_e2e_performance() {
|
||||||
let temp_dir = std::env::temp_dir().join(format!("mcp_e2e_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()));
|
let temp_dir = std::env::temp_dir().join(format!(
|
||||||
|
"mcp_e2e_{}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
));
|
||||||
std::fs::create_dir_all(&temp_dir).unwrap();
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
||||||
|
|
||||||
let test_port = "3042"; // Use a distinct port
|
let test_port = "3042"; // Use a distinct port
|
||||||
let test_auth_token = "test-token-12345";
|
let test_auth_token = "test-token-12345";
|
||||||
|
|
||||||
// Since tests run from inside `target/debug/deps`, and `cargo test` does not guarantee
|
// Since tests run from inside `target/debug/deps`, and `cargo test` does not guarantee
|
||||||
// `env!("CARGO_BIN_EXE_name")` works correctly for binaries compiled in other crates without build dependencies,
|
// `env!("CARGO_BIN_EXE_name")` works correctly for binaries compiled in other crates without build dependencies,
|
||||||
// we use `CARGO_MANIFEST_DIR` (which points to `stub`) to reliably locate the workspace `target/debug`.
|
// we use `CARGO_MANIFEST_DIR` (which points to `stub`) to reliably locate the workspace `target/debug`.
|
||||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
let debug_dir = manifest_dir.parent().unwrap().join("target").join("debug");
|
let debug_dir = manifest_dir.parent().unwrap().join("target").join("debug");
|
||||||
|
|
||||||
let server_exe = debug_dir.join(format!("mcp-memory-server{}", std::env::consts::EXE_SUFFIX));
|
let server_exe = debug_dir.join(format!("mcp-memory-server{}", std::env::consts::EXE_SUFFIX));
|
||||||
let nvim_name = if cfg!(windows) {
|
let nvim_name = if cfg!(windows) {
|
||||||
"mcp-memory-win-nvim"
|
"mcp-memory-win-nvim"
|
||||||
@@ -41,21 +46,24 @@ async fn test_full_system_e2e_performance() {
|
|||||||
};
|
};
|
||||||
let nvim_exe = debug_dir.join(format!("{}{}", nvim_name, std::env::consts::EXE_SUFFIX));
|
let nvim_exe = debug_dir.join(format!("{}{}", nvim_name, std::env::consts::EXE_SUFFIX));
|
||||||
let stub_exe = debug_dir.join(format!("mcp-memory-stub{}", std::env::consts::EXE_SUFFIX));
|
let stub_exe = debug_dir.join(format!("mcp-memory-stub{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
|
||||||
assert!(server_exe.exists(), "Server not found at {:?}", server_exe);
|
assert!(server_exe.exists(), "Server not found at {:?}", server_exe);
|
||||||
assert!(nvim_exe.exists(), "Nvim not found at {:?}", nvim_exe);
|
assert!(nvim_exe.exists(), "Nvim not found at {:?}", nvim_exe);
|
||||||
assert!(stub_exe.exists(), "Stub not found at {:?}", stub_exe);
|
assert!(stub_exe.exists(), "Stub not found at {:?}", stub_exe);
|
||||||
|
|
||||||
// 1. Start Server
|
// 1. Start Server
|
||||||
let mut server = Command::new(&server_exe).arg("--daemon")
|
let mut server = Command::new(&server_exe)
|
||||||
.env("MCP_PORT", test_port).env("RUST_LOG", "debug")
|
.arg("--daemon")
|
||||||
|
.env("MCP_PORT", test_port)
|
||||||
|
.env("RUST_LOG", "debug")
|
||||||
.env("MCP_MEMORY_STORE_DIR", temp_dir.to_str().unwrap())
|
.env("MCP_MEMORY_STORE_DIR", temp_dir.to_str().unwrap())
|
||||||
.env("MCP_AUTH_TOKEN", test_auth_token).env("RUST_LOG", "debug")
|
.env("MCP_AUTH_TOKEN", test_auth_token)
|
||||||
|
.env("RUST_LOG", "debug")
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to start server");
|
.expect("Failed to start server");
|
||||||
|
|
||||||
// Give server time to generate TLS cert and start
|
// Give server time to generate TLS cert and start
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
@@ -63,28 +71,32 @@ async fn test_full_system_e2e_performance() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let mut started = false;
|
let mut started = false;
|
||||||
for _ in 0..30 {
|
for _ in 0..30 {
|
||||||
if let Ok(resp) = client.get(format!("http://127.0.0.1:{}/health", test_port)).send().await {
|
if let Ok(resp) = client
|
||||||
if resp.status().is_success() {
|
.get(format!("http://127.0.0.1:{}/health", test_port))
|
||||||
started = true;
|
.send()
|
||||||
break;
|
.await
|
||||||
}
|
&& resp.status().is_success()
|
||||||
|
{
|
||||||
|
started = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
assert!(started, "Server failed to start in time");
|
assert!(started, "Server failed to start in time");
|
||||||
|
|
||||||
// 2. Start Stub
|
// 2. Start Stub
|
||||||
let mut stub = Command::new(&stub_exe)
|
let mut stub = Command::new(&stub_exe)
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
.arg(format!("http://127.0.0.1:{}", test_port))
|
.arg(format!("http://127.0.0.1:{}", test_port))
|
||||||
.env("MCP_MEMORY_STORE_DIR", temp_dir.to_str().unwrap())
|
.env("MCP_MEMORY_STORE_DIR", temp_dir.to_str().unwrap())
|
||||||
.env("MCP_AUTH_TOKEN", test_auth_token).env("RUST_LOG", "debug")
|
.env("MCP_AUTH_TOKEN", test_auth_token)
|
||||||
|
.env("RUST_LOG", "debug")
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to start stub");
|
.expect("Failed to start stub");
|
||||||
|
|
||||||
let mut stub_stdin = stub.stdin.take().unwrap();
|
let mut stub_stdin = stub.stdin.take().unwrap();
|
||||||
let mut stub_stdout = BufReader::new(stub.stdout.take().unwrap());
|
let mut stub_stdout = BufReader::new(stub.stdout.take().unwrap());
|
||||||
|
|
||||||
@@ -111,7 +123,7 @@ async fn test_full_system_e2e_performance() {
|
|||||||
"params": {},
|
"params": {},
|
||||||
"id": i
|
"id": i
|
||||||
});
|
});
|
||||||
|
|
||||||
// Alternate between LSP header format and JSONL format
|
// Alternate between LSP header format and JSONL format
|
||||||
if i % 2 == 0 {
|
if i % 2 == 0 {
|
||||||
send_message(&mut stub_stdin, tools_req);
|
send_message(&mut stub_stdin, tools_req);
|
||||||
@@ -121,7 +133,8 @@ async fn test_full_system_e2e_performance() {
|
|||||||
stub_stdin.flush().unwrap();
|
stub_stdin.flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut resp = read_message(&mut stub_stdout).expect("Failed to read rapid response from stub");
|
let mut resp =
|
||||||
|
read_message(&mut stub_stdout).expect("Failed to read rapid response from stub");
|
||||||
while resp.get("id").is_none() || resp["id"].is_null() {
|
while resp.get("id").is_none() || resp["id"].is_null() {
|
||||||
resp = read_message(&mut stub_stdout).expect("Failed to read rapid response from stub");
|
resp = read_message(&mut stub_stdout).expect("Failed to read rapid response from stub");
|
||||||
}
|
}
|
||||||
@@ -140,7 +153,7 @@ async fn test_full_system_e2e_performance() {
|
|||||||
"params": {},
|
"params": {},
|
||||||
"id": i
|
"id": i
|
||||||
});
|
});
|
||||||
|
|
||||||
if i % 2 == 0 {
|
if i % 2 == 0 {
|
||||||
send_message(&mut nvim_stdin, tools_req);
|
send_message(&mut nvim_stdin, tools_req);
|
||||||
} else {
|
} else {
|
||||||
@@ -149,9 +162,11 @@ async fn test_full_system_e2e_performance() {
|
|||||||
nvim_stdin.flush().unwrap();
|
nvim_stdin.flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut resp = read_message(&mut nvim_stdout).expect("Failed to read rapid response from win-nvim");
|
let mut resp =
|
||||||
|
read_message(&mut nvim_stdout).expect("Failed to read rapid response from win-nvim");
|
||||||
while resp.get("id").is_none() || resp["id"].is_null() {
|
while resp.get("id").is_none() || resp["id"].is_null() {
|
||||||
resp = read_message(&mut nvim_stdout).expect("Failed to read rapid response from win-nvim");
|
resp = read_message(&mut nvim_stdout)
|
||||||
|
.expect("Failed to read rapid response from win-nvim");
|
||||||
}
|
}
|
||||||
assert_eq!(resp["id"], i);
|
assert_eq!(resp["id"], i);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ fn get_stub_exe() -> std::path::PathBuf {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_stub_connection_refused() {
|
async fn test_stub_connection_refused() {
|
||||||
let _ = std::process::Command::new("cargo").arg("build").arg("--bin").arg("mcp-memory-stub").status();
|
let _ = std::process::Command::new("cargo")
|
||||||
|
.arg("build")
|
||||||
|
.arg("--bin")
|
||||||
|
.arg("mcp-memory-stub")
|
||||||
|
.status();
|
||||||
|
|
||||||
let target = "http://127.0.0.1:49999";
|
let target = "http://127.0.0.1:49999";
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut child = Command::new(get_stub_exe())
|
let mut child = Command::new(get_stub_exe())
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
@@ -21,19 +25,27 @@ async fn test_stub_connection_refused() {
|
|||||||
.stdin(Stdio::null()) // close stdin immediately to simulate EOF
|
.stdin(Stdio::null()) // close stdin immediately to simulate EOF
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to execute stub");
|
.expect("Failed to execute stub");
|
||||||
|
|
||||||
let res = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
let res = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
assert!(res.is_ok(), "Stub hung on connection refused! Took {:?}", elapsed);
|
assert!(
|
||||||
|
res.is_ok(),
|
||||||
|
"Stub hung on connection refused! Took {:?}",
|
||||||
|
elapsed
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_stub_handles_eof_cleanly() {
|
async fn test_stub_handles_eof_cleanly() {
|
||||||
let _ = std::process::Command::new("cargo").arg("build").arg("--bin").arg("mcp-memory-stub").status();
|
let _ = std::process::Command::new("cargo")
|
||||||
|
.arg("build")
|
||||||
|
.arg("--bin")
|
||||||
|
.arg("mcp-memory-stub")
|
||||||
|
.status();
|
||||||
|
|
||||||
let target = "http://127.0.0.1:49998";
|
let target = "http://127.0.0.1:49998";
|
||||||
|
|
||||||
let mut child = Command::new(get_stub_exe())
|
let mut child = Command::new(get_stub_exe())
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
.arg(target)
|
.arg(target)
|
||||||
@@ -42,28 +54,32 @@ async fn test_stub_handles_eof_cleanly() {
|
|||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to execute stub");
|
.expect("Failed to execute stub");
|
||||||
|
|
||||||
if let Some(mut stdin) = child.stdin.take() {
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
let msg = "Content-Length: 51\r\n\r\n{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"params\":{},\"id\":1}";
|
let msg = "Content-Length: 51\r\n\r\n{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"params\":{},\"id\":1}";
|
||||||
stdin.write_all(msg.as_bytes()).await.unwrap();
|
stdin.write_all(msg.as_bytes()).await.unwrap();
|
||||||
} // stdin dropped here
|
} // stdin dropped here
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let res = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
let res = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
assert!(res.is_ok(), "Stub hung after EOF! Took {:?}", elapsed);
|
assert!(res.is_ok(), "Stub hung after EOF! Took {:?}", elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_stub_sse_fallback_failure() {
|
async fn test_stub_sse_fallback_failure() {
|
||||||
let _ = std::process::Command::new("cargo").arg("build").arg("--bin").arg("mcp-memory-stub").status();
|
let _ = std::process::Command::new("cargo")
|
||||||
|
.arg("build")
|
||||||
|
.arg("--bin")
|
||||||
|
.arg("mcp-memory-stub")
|
||||||
|
.status();
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
let local_addr = listener.local_addr().unwrap();
|
let local_addr = listener.local_addr().unwrap();
|
||||||
let target = format!("http://127.0.0.1:{}", local_addr.port());
|
let target = format!("http://127.0.0.1:{}", local_addr.port());
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Ok((mut socket, _)) = listener.accept().await {
|
while let Ok((mut socket, _)) = listener.accept().await {
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
@@ -77,12 +93,16 @@ async fn test_stub_sse_fallback_failure() {
|
|||||||
let mut child = Command::new(get_stub_exe())
|
let mut child = Command::new(get_stub_exe())
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
.arg(target)
|
.arg(target)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to execute stub");
|
.expect("Failed to execute stub");
|
||||||
|
|
||||||
let res = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
let res = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
assert!(res.is_ok(), "Stub hung on fallback failure! Took {:?}", elapsed);
|
assert!(
|
||||||
|
res.is_ok(),
|
||||||
|
"Stub hung on fallback failure! Took {:?}",
|
||||||
|
elapsed
|
||||||
|
);
|
||||||
}
|
}
|
||||||
+14
-9
@@ -2,19 +2,24 @@ use std::process::Command;
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let git_hash = Command::new("git")
|
let git_hash = Command::new("git")
|
||||||
.args(&["rev-parse", "--short", "HEAD"])
|
.args(["rev-parse", "--short", "HEAD"])
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
|
|
||||||
let git_date = Command::new("git")
|
|
||||||
.args(&["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
let version = format!("{} ({} {})", env!("CARGO_PKG_VERSION"), git_date.trim(), git_hash.trim());
|
let git_date = Command::new("git")
|
||||||
|
.args(["log", "-1", "--format=%cd", "--date=format:%Y.%m.%d"])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|out| String::from_utf8(out.stdout).ok())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
let version = format!(
|
||||||
|
"{} ({} {})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
git_date.trim(),
|
||||||
|
git_hash.trim()
|
||||||
|
);
|
||||||
println!("cargo:rustc-env=APP_VERSION={}", version);
|
println!("cargo:rustc-env=APP_VERSION={}", version);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ fn send_message(stdin: &mut std::process::ChildStdin, msg: Value) {
|
|||||||
fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
|
fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
|
||||||
let mut reader = BufReader::new(stdout);
|
let mut reader = BufReader::new(stdout);
|
||||||
let mut length = 0;
|
let mut length = 0;
|
||||||
|
|
||||||
// Read headers
|
// Read headers
|
||||||
loop {
|
loop {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
@@ -27,16 +27,16 @@ fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
|
|||||||
length = len_str.parse().unwrap_or(0);
|
length = len_str.parse().unwrap_or(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read body
|
// Read body
|
||||||
let mut buf = vec![0u8; length];
|
let mut buf = vec![0u8; length];
|
||||||
reader.read_exact(&mut buf).unwrap();
|
reader.read_exact(&mut buf).unwrap();
|
||||||
let body_str = String::from_utf8_lossy(&buf);
|
let body_str = String::from_utf8_lossy(&buf);
|
||||||
|
|
||||||
Some(serde_json::from_str(&body_str).unwrap())
|
Some(serde_json::from_str(&body_str).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +87,12 @@ fn test_mcp_initialization_and_tools_list() {
|
|||||||
let s = serde_json::to_string(&init_req).unwrap();
|
let s = serde_json::to_string(&init_req).unwrap();
|
||||||
stdin.write_all(format!("{}\n", s).as_bytes()).unwrap();
|
stdin.write_all(format!("{}\n", s).as_bytes()).unwrap();
|
||||||
stdin.flush().unwrap();
|
stdin.flush().unwrap();
|
||||||
|
|
||||||
let init_resp = read_message(&mut stdout).expect("Failed to read initialize response");
|
let init_resp = read_message(&mut stdout).expect("Failed to read initialize response");
|
||||||
|
|
||||||
assert_eq!(init_resp["jsonrpc"], "2.0");
|
assert_eq!(init_resp["jsonrpc"], "2.0");
|
||||||
assert_eq!(init_resp["id"], 1);
|
assert_eq!(init_resp["id"], 1);
|
||||||
|
|
||||||
// Verify capabilities
|
// Verify capabilities
|
||||||
let capabilities = &init_resp["result"]["capabilities"];
|
let capabilities = &init_resp["result"]["capabilities"];
|
||||||
assert_eq!(capabilities["tools"], serde_json::json!({}));
|
assert_eq!(capabilities["tools"], serde_json::json!({}));
|
||||||
@@ -104,17 +104,19 @@ fn test_mcp_initialization_and_tools_list() {
|
|||||||
"params": {},
|
"params": {},
|
||||||
"id": 2
|
"id": 2
|
||||||
});
|
});
|
||||||
|
|
||||||
send_message(&mut stdin, tools_req);
|
send_message(&mut stdin, tools_req);
|
||||||
|
|
||||||
let tools_resp = read_message(&mut stdout).expect("Failed to read tools/list response");
|
let tools_resp = read_message(&mut stdout).expect("Failed to read tools/list response");
|
||||||
|
|
||||||
assert_eq!(tools_resp["jsonrpc"], "2.0");
|
assert_eq!(tools_resp["jsonrpc"], "2.0");
|
||||||
assert_eq!(tools_resp["id"], 2);
|
assert_eq!(tools_resp["id"], 2);
|
||||||
|
|
||||||
let tools = tools_resp["result"]["tools"].as_array().expect("result.tools must be an array");
|
let tools = tools_resp["result"]["tools"]
|
||||||
|
.as_array()
|
||||||
|
.expect("result.tools must be an array");
|
||||||
assert!(!tools.is_empty(), "Server must expose at least one tool");
|
assert!(!tools.is_empty(), "Server must expose at least one tool");
|
||||||
|
|
||||||
let has_get_active_buffer = tools.iter().any(|t| t["name"] == "nvim_get_active_buffer");
|
let has_get_active_buffer = tools.iter().any(|t| t["name"] == "nvim_get_active_buffer");
|
||||||
assert!(has_get_active_buffer, "Missing nvim_get_active_buffer tool");
|
assert!(has_get_active_buffer, "Missing nvim_get_active_buffer tool");
|
||||||
|
|
||||||
@@ -130,14 +132,17 @@ fn test_mcp_initialization_and_tools_list() {
|
|||||||
},
|
},
|
||||||
"id": 3
|
"id": 3
|
||||||
});
|
});
|
||||||
|
|
||||||
send_message(&mut stdin, call_req);
|
send_message(&mut stdin, call_req);
|
||||||
|
|
||||||
let call_resp = read_message(&mut stdout).expect("Failed to read tools/call response");
|
let call_resp = read_message(&mut stdout).expect("Failed to read tools/call response");
|
||||||
|
|
||||||
assert_eq!(call_resp["jsonrpc"], "2.0");
|
assert_eq!(call_resp["jsonrpc"], "2.0");
|
||||||
assert_eq!(call_resp["id"], 3);
|
assert_eq!(call_resp["id"], 3);
|
||||||
assert!(call_resp.get("error").is_some(), "Expected an error response since Neovim shouldn't be running");
|
assert!(
|
||||||
|
call_resp.get("error").is_some(),
|
||||||
|
"Expected an error response since Neovim shouldn't be running"
|
||||||
|
);
|
||||||
assert_eq!(call_resp["error"]["code"], -32603); // Internal Error
|
assert_eq!(call_resp["error"]["code"], -32603); // Internal Error
|
||||||
|
|
||||||
child.kill().expect("Failed to kill child");
|
child.kill().expect("Failed to kill child");
|
||||||
|
|||||||
Reference in new issue
Block a user