Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
368f28e593 | ||
|
|
6011da95f6 | ||
|
|
9009f127a9 | ||
|
|
f8c0d58415 | ||
|
|
c9ba5e79dd | ||
|
|
ce17ffcf42 |
No files matched your search
Generated
+11
@@ -1375,6 +1375,8 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"dirs 7.0.0",
|
"dirs 7.0.0",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"mcp-stdio",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1404,6 +1406,14 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mcp-stdio"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "measure_time"
|
name = "measure_time"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -1527,6 +1537,7 @@ name = "nvim-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 7.0.0",
|
"dirs 7.0.0",
|
||||||
|
"mcp-stdio",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rmpv",
|
"rmpv",
|
||||||
|
|||||||
+1
-1
@@ -4,5 +4,5 @@ members = [
|
|||||||
"stub",
|
"stub",
|
||||||
"win-nvim",
|
"win-nvim",
|
||||||
"linux-nvim"
|
"linux-nvim"
|
||||||
, "nvim-core"]
|
, "nvim-core", "mcp-stdio"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
@@ -20,7 +20,12 @@ if (Test-Path $serverExe) {
|
|||||||
& $serverExe --exit 2>$null
|
& $serverExe --exit 2>$null
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Invoke-RestMethod -Uri "https://127.0.0.1:3000/shutdown" -Method Post -SkipCertificateCheck -ErrorAction Stop | Out-Null
|
$tokenPath = "C:\Users\reazul.ashraf\.gemini\mcp_memory\admin.token"
|
||||||
|
$headers = @{}
|
||||||
|
if (Test-Path $tokenPath) {
|
||||||
|
$headers["Authorization"] = "Bearer $(Get-Content -Path $tokenPath -Raw)".Trim()
|
||||||
|
}
|
||||||
|
Invoke-RestMethod -Uri "https://127.0.0.1:3000/shutdown" -Method Post -Headers $headers -SkipCertificateCheck -ErrorAction Stop | Out-Null
|
||||||
} catch {
|
} catch {
|
||||||
# The response often ends prematurely because the server abruptly kills its own process during shutdown. This is expected.
|
# The response often ends prematurely because the server abruptly kills its own process during shutdown. This is expected.
|
||||||
}
|
}
|
||||||
@@ -63,7 +68,7 @@ function Deploy-If-Needed {
|
|||||||
if ($EnvName -eq 'WSL') {
|
if ($EnvName -eq 'WSL') {
|
||||||
# Translate source path to WSL path
|
# Translate source path to WSL path
|
||||||
$wslSource = wsl.exe -d Ubuntu -e wslpath -u $Source
|
$wslSource = wsl.exe -d Ubuntu -e wslpath -u $Source
|
||||||
wsl.exe -d Ubuntu -e bash -c "mv $Dest ${Dest}.`date +%s`.old 2>/dev/null || true; cp $wslSource $Dest && chmod +x $Dest"
|
wsl.exe -d Ubuntu -e bash -c "mv $Dest ${Dest}.\$(date +%s).old 2>/dev/null || true; cp $wslSource $Dest && chmod +x $Dest"
|
||||||
} else {
|
} else {
|
||||||
if (Test-Path $Dest) {
|
if (Test-Path $Dest) {
|
||||||
$rand = Get-Random
|
$rand = Get-Random
|
||||||
|
|||||||
+6
-1
@@ -1,6 +1,11 @@
|
|||||||
try {
|
try {
|
||||||
Write-Host "Gracefully shutting down server..."
|
Write-Host "Gracefully shutting down server..."
|
||||||
Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:3000/shutdown" -ErrorAction SilentlyContinue
|
$tokenPath = "C:\Users\reazul.ashraf\.gemini\mcp_memory\admin.token"
|
||||||
|
$headers = @{}
|
||||||
|
if (Test-Path $tokenPath) {
|
||||||
|
$headers["Authorization"] = "Bearer $(Get-Content -Path $tokenPath -Raw)".Trim()
|
||||||
|
}
|
||||||
|
Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:3000/shutdown" -Headers $headers -ErrorAction SilentlyContinue | Out-Null
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 2
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host "Server might already be down or unreachable."
|
Write-Host "Server might already be down or unreachable."
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "mcp-stdio"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.53.1", features = ["io-util"] }
|
||||||
|
tracing = "0.1.44"
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
|
||||||
|
|
||||||
|
/// Reads an MCP (NDJSON or LSP Content-Length prefixed) message from a buffered async reader.
|
||||||
|
/// Returns the raw JSON string payload if successful, or None on EOF or error.
|
||||||
|
pub async fn read_mcp_message<R: tokio::io::AsyncRead + Unpin>(
|
||||||
|
stdin: &mut BufReader<R>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut length = 0;
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
if stdin.read_line(&mut line).await.unwrap_or(0) == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.starts_with('{') {
|
||||||
|
return Some(line.trim_end().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = line.trim_end();
|
||||||
|
if line.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower_line = line.to_lowercase();
|
||||||
|
if let Some(len_str) = lower_line.strip_prefix("content-length:") {
|
||||||
|
length = len_str.trim().parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = vec![0; length];
|
||||||
|
if stdin.read_exact(&mut buffer).await.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
String::from_utf8(buffer).ok()
|
||||||
|
}
|
||||||
@@ -14,4 +14,5 @@ tracing-appender = "0.2.5"
|
|||||||
tracing-subscriber = "0.3.23"
|
tracing-subscriber = "0.3.23"
|
||||||
dirs = "7.0.0"
|
dirs = "7.0.0"
|
||||||
rmcp = { version = "3.4.0", features = ["server"] }
|
rmcp = { version = "3.4.0", features = ["server"] }
|
||||||
|
mcp-stdio = { version = "0.1.0", path = "../mcp-stdio" }
|
||||||
|
|
||||||
+176
-233
@@ -20,47 +20,7 @@ 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> {
|
|
||||||
let mut length = 0;
|
|
||||||
loop {
|
|
||||||
let mut line = String::new();
|
|
||||||
if stdin.read_line(&mut line).await.unwrap_or(0) == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.starts_with('{') {
|
|
||||||
return match serde_json::from_str::<JsonRpcRequest>(line.trim_end()) {
|
|
||||||
Ok(req) => Some(req),
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to parse JSON-RPC request from JSONL: {}. Payload: {}",
|
|
||||||
e,
|
|
||||||
line
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = line.trim_end();
|
|
||||||
if line.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let lower_line = line.to_lowercase();
|
|
||||||
if let Some(len_str) = lower_line.strip_prefix("content-length:") {
|
|
||||||
length = len_str.trim().parse().unwrap_or(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if length == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut buffer = vec![0; length];
|
|
||||||
stdin.read_exact(&mut buffer).await.unwrap_or(0);
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -99,7 +59,7 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
|
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) = tokio::fs::read_to_string(&path).await {
|
||||||
let p = content.trim().to_string();
|
let p = content.trim().to_string();
|
||||||
if !p.is_empty() {
|
if !p.is_empty() {
|
||||||
if p.starts_with(r"\\.\pipe\") {
|
if p.starts_with(r"\\.\pipe\") {
|
||||||
@@ -113,8 +73,8 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tracing::warn!("active_nvim.txt missing or invalid, falling back to pipe discovery");
|
tracing::warn!("active_nvim.txt missing or invalid, falling back to pipe discovery");
|
||||||
if let Ok(dir) = std::fs::read_dir(r"\\.\pipe\") {
|
if let Ok(mut dir) = tokio::fs::read_dir(r"\\.\pipe\").await {
|
||||||
for entry in dir.flatten() {
|
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||||
let name = entry.file_name();
|
let name = entry.file_name();
|
||||||
let name_str = name.to_string_lossy();
|
let name_str = name.to_string_lossy();
|
||||||
if name_str.starts_with("nvim.") {
|
if name_str.starts_with("nvim.") {
|
||||||
@@ -129,7 +89,7 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
async fn get_socket_path() -> Result<String, String> {
|
async fn get_socket_path() -> Result<String, String> {
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
if let Ok(home) = std::env::var("HOME") {
|
||||||
let path = format!("{}/.gemini/active_nvim.txt", home);
|
let path = format!("{}/.gemini/active_nvim.txt", home);
|
||||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
||||||
let p = content.trim().to_string();
|
let p = content.trim().to_string();
|
||||||
if !p.is_empty() {
|
if !p.is_empty() {
|
||||||
return Ok(p);
|
return Ok(p);
|
||||||
@@ -137,12 +97,12 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(entries) = std::fs::read_dir("/tmp") {
|
if let Ok(mut entries) = tokio::fs::read_dir("/tmp").await {
|
||||||
for entry in entries.flatten() {
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
if let Ok(name) = entry.file_name().into_string() {
|
if let Ok(name) = entry.file_name().into_string() {
|
||||||
if name.starts_with("nvim.") {
|
if name.starts_with("nvim.") {
|
||||||
let path = entry.path().join("0");
|
let path = entry.path().join("0");
|
||||||
if path.exists() {
|
if tokio::fs::try_exists(&path).await.unwrap_or(false) {
|
||||||
return Ok(path.to_string_lossy().to_string());
|
return Ok(path.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,10 +111,128 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
Err("Could not find Neovim socket".to_string())
|
Err("Could not find Neovim socket".to_string())
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
use std::sync::LazyLock;
|
||||||
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
use std::sync::Arc;
|
||||||
use tokio::net::windows::named_pipe::ClientOptions;
|
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct NvimRequest {
|
||||||
|
pub msgid_str: String,
|
||||||
|
pub req: rmpv::Value,
|
||||||
|
pub reply: oneshot::Sender<Result<rmpv::Value, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static NVIM_CONN: LazyLock<Arc<Mutex<Option<mpsc::Sender<NvimRequest>>>>> = LazyLock::new(|| Arc::new(Mutex::new(None)));
|
||||||
|
|
||||||
|
async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
||||||
|
let mut conn_lock = NVIM_CONN.lock().await;
|
||||||
|
if let Some(sender) = conn_lock.as_ref() {
|
||||||
|
if !sender.is_closed() {
|
||||||
|
return Ok(sender.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Establishing new persistent connection to Neovim");
|
||||||
|
let socket_path = get_socket_path().await?;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
let stream = {
|
||||||
|
use tokio::net::windows::named_pipe::ClientOptions;
|
||||||
|
ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let stream = {
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut read_half, mut write_half) = tokio::io::split(stream);
|
||||||
|
let (tx, mut rx) = mpsc::channel::<NvimRequest>(32);
|
||||||
|
let pending_requests: Arc<Mutex<HashMap<String, oneshot::Sender<Result<rmpv::Value, String>>>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
// Write task
|
||||||
|
let pending_clone = Arc::clone(&pending_requests);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(req) = rx.recv().await {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
if let Err(e) = rmpv::encode::write_value(&mut buf, &req.req) {
|
||||||
|
let _ = req.reply.send(Err(e.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_clone.lock().await.insert(req.msgid_str.clone(), req.reply);
|
||||||
|
|
||||||
|
if write_half.write_all(&buf).await.is_err() {
|
||||||
|
tracing::error!("Failed to write to Neovim socket");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read task
|
||||||
|
let pending_clone2 = Arc::clone(&pending_requests);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut resp_buf = Vec::new();
|
||||||
|
let mut chunk = vec![0u8; 8192];
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut cursor = std::io::Cursor::new(&resp_buf[offset..]);
|
||||||
|
match rmpv::decode::read_value(&mut cursor) {
|
||||||
|
Ok(val) => {
|
||||||
|
offset += cursor.position() as usize;
|
||||||
|
|
||||||
|
if let rmpv::Value::Array(ref arr) = val {
|
||||||
|
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) {
|
||||||
|
let msgid = &arr[1];
|
||||||
|
let msgid_str = format!("{:?}", msgid);
|
||||||
|
|
||||||
|
if let Some(reply_sender) = pending_clone2.lock().await.remove(&msgid_str) {
|
||||||
|
let _ = reply_sender.send(Ok(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim buffer if it gets too large
|
||||||
|
if offset > 1024 * 1024 {
|
||||||
|
resp_buf.drain(..offset);
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if offset > 0 {
|
||||||
|
resp_buf.drain(..offset);
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let read_future = read_half.read(&mut chunk);
|
||||||
|
match tokio::time::timeout(tokio::time::Duration::from_secs(60), read_future).await {
|
||||||
|
Ok(Ok(n)) if n > 0 => {
|
||||||
|
resp_buf.extend_from_slice(&chunk[..n]);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::error!("Neovim socket read loop closed or timeout");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup pending requests on disconnect
|
||||||
|
let mut pending = pending_clone2.lock().await;
|
||||||
|
for (_, sender) in pending.drain() {
|
||||||
|
let _ = sender.send(Err("Connection closed".to_string()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
*conn_lock = Some(tx.clone());
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
||||||
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
||||||
if arr.len() > 1 {
|
if arr.len() > 1 {
|
||||||
arr[1].clone()
|
arr[1].clone()
|
||||||
@@ -165,119 +243,23 @@ async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
|||||||
rmpv::Value::Nil
|
rmpv::Value::Nil
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("Connecting to neovim pipe");
|
let msgid_str = format!("{:?}", msgid);
|
||||||
let socket_path = get_socket_path().await?;
|
let tx = get_nvim_connection().await?;
|
||||||
let mut client = ClientOptions::new()
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
.open(&socket_path)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
tx.send(NvimRequest {
|
||||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
msgid_str,
|
||||||
tracing::info!("Sending RPC request to neovim (msgid: {})", msgid);
|
req,
|
||||||
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
reply: reply_tx,
|
||||||
|
}).await.map_err(|_| "Failed to send request to Neovim connection manager")?;
|
||||||
|
|
||||||
let mut resp_buf = Vec::new();
|
match tokio::time::timeout(tokio::time::Duration::from_secs(10), reply_rx).await {
|
||||||
let mut chunk = vec![0u8; 8192];
|
Ok(Ok(res)) => res,
|
||||||
let mut offset = 0;
|
Ok(Err(_)) => Err("Response channel dropped".to_string()),
|
||||||
|
Err(_) => Err("Timeout waiting for Neovim response".to_string()),
|
||||||
loop {
|
|
||||||
let mut cursor = std::io::Cursor::new(&resp_buf[offset..]);
|
|
||||||
match rmpv::decode::read_value(&mut cursor) {
|
|
||||||
Ok(val) => {
|
|
||||||
offset += cursor.position() as usize;
|
|
||||||
|
|
||||||
if let rmpv::Value::Array(ref arr) = val {
|
|
||||||
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid
|
|
||||||
{
|
|
||||||
tracing::info!("Received RPC response from neovim (msgid: {})", msgid);
|
|
||||||
return Ok(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
let read_future = client.read(&mut chunk);
|
|
||||||
match tokio::time::timeout(tokio::time::Duration::from_secs(5), read_future).await {
|
|
||||||
Ok(Ok(n)) => {
|
|
||||||
if n == 0 {
|
|
||||||
return Err("Connection closed".into());
|
|
||||||
}
|
|
||||||
resp_buf.extend_from_slice(&chunk[..n]);
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => return Err(e.to_string()),
|
|
||||||
Err(_) => {
|
|
||||||
tracing::error!("Timeout waiting for Neovim response (msgid: {})", msgid);
|
|
||||||
return Err("Timeout waiting for Neovim response".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
|
||||||
use tokio::net::UnixStream;
|
|
||||||
|
|
||||||
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
|
||||||
if arr.len() > 1 {
|
|
||||||
arr[1].clone()
|
|
||||||
} else {
|
|
||||||
rmpv::Value::Nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rmpv::Value::Nil
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!("Connecting to neovim socket");
|
|
||||||
let socket_path = get_socket_path().await?;
|
|
||||||
let mut stream = UnixStream::connect(socket_path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
|
||||||
tracing::info!("Sending RPC request to neovim (msgid: {})", msgid);
|
|
||||||
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let mut resp_buf = Vec::new();
|
|
||||||
let mut chunk = vec![0u8; 8192];
|
|
||||||
let mut offset = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let mut cursor = std::io::Cursor::new(&resp_buf[offset..]);
|
|
||||||
match rmpv::decode::read_value(&mut cursor) {
|
|
||||||
Ok(val) => {
|
|
||||||
offset += cursor.position() as usize;
|
|
||||||
|
|
||||||
if let rmpv::Value::Array(ref arr) = val {
|
|
||||||
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) && arr[1] == msgid
|
|
||||||
{
|
|
||||||
tracing::info!("Received RPC response from neovim (msgid: {})", msgid);
|
|
||||||
return Ok(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
let read_future = stream.read(&mut chunk);
|
|
||||||
match tokio::time::timeout(tokio::time::Duration::from_secs(5), read_future).await {
|
|
||||||
Ok(Ok(n)) => {
|
|
||||||
if n == 0 {
|
|
||||||
return Err("Connection closed".into());
|
|
||||||
}
|
|
||||||
resp_buf.extend_from_slice(&chunk[..n]);
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => return Err(e.to_string()),
|
|
||||||
Err(_) => {
|
|
||||||
tracing::error!("Timeout waiting for Neovim response (msgid: {})", msgid);
|
|
||||||
return Err("Timeout waiting for Neovim response".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
|
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
|
||||||
use rmpv::Value as RmpValue;
|
use rmpv::Value as RmpValue;
|
||||||
let req = RmpValue::Array(vec![
|
let req = RmpValue::Array(vec![
|
||||||
@@ -494,6 +476,20 @@ async fn execute_nvim_lua(code: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
Err("Invalid response".to_string())
|
Err("Invalid response".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! send_text_result {
|
||||||
|
($id:expr, $text:expr) => {
|
||||||
|
send_response(JsonRpcResponse {
|
||||||
|
jsonrpc: "2.0".to_string(),
|
||||||
|
id: $id,
|
||||||
|
result: Some(json!({
|
||||||
|
"content": [{"type": "text", "text": $text}]
|
||||||
|
})),
|
||||||
|
error: None,
|
||||||
|
}).await;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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!(
|
println!(
|
||||||
@@ -508,17 +504,25 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
tracing::info!("{} MCP server started", app_name);
|
tracing::info!("{} MCP server started", app_name);
|
||||||
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
|
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
|
||||||
loop {
|
loop {
|
||||||
let msg = match read_message(&mut stdin).await {
|
let raw_msg = match mcp_stdio::read_mcp_message(&mut stdin).await {
|
||||||
Some(m) => {
|
Some(m) => m,
|
||||||
tracing::info!("Received message method: {}", m.method);
|
|
||||||
m
|
|
||||||
}
|
|
||||||
None => {
|
None => {
|
||||||
tracing::info!("Stdin closed, exiting loop");
|
tracing::info!("Stdin closed, exiting loop");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let msg = match serde_json::from_str::<JsonRpcRequest>(&raw_msg) {
|
||||||
|
Ok(m) => {
|
||||||
|
tracing::info!("Received message method: {}", m.method);
|
||||||
|
m
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to parse JSON-RPC request from JSONL: {}. Payload: {}", e, raw_msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let app_name = app_name.to_string();
|
let app_name = app_name.to_string();
|
||||||
let app_version = app_version.to_string();
|
let app_version = app_version.to_string();
|
||||||
|
|
||||||
@@ -659,14 +663,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
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 {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
send_response(JsonRpcResponse {
|
send_text_result!(id.clone(), format!("Successfully jumped to {} line {}", file, line));
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": format!("Successfully jumped to {} line {}", file, line)}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
}).await;
|
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -676,43 +673,19 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
}
|
}
|
||||||
"nvim_get_active_buffer" => match get_nvim_active_buffer().await {
|
"nvim_get_active_buffer" => match get_nvim_active_buffer().await {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
send_response(JsonRpcResponse {
|
send_text_result!(id.clone(), content);
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": content}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
})
|
|
||||||
.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_text_result!(id.clone(), content);
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": content}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
})
|
|
||||||
.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_text_result!(id.clone(), content);
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": content}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
},
|
},
|
||||||
@@ -723,14 +696,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
) {
|
) {
|
||||||
match set_nvim_diagnostics(line, message).await {
|
match set_nvim_diagnostics(line, message).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
send_response(JsonRpcResponse {
|
send_text_result!(id.clone(), format!("Successfully set diagnostic on line {}", line));
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": format!("Successfully set diagnostic on line {}", line)}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
}).await;
|
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -752,15 +718,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
"#;
|
"#;
|
||||||
match execute_nvim_lua(lua_code).await {
|
match execute_nvim_lua(lua_code).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
send_response(JsonRpcResponse {
|
send_text_result!(id.clone(), result);
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": result}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -783,15 +741,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
"#;
|
"#;
|
||||||
match execute_nvim_lua(lua_code).await {
|
match execute_nvim_lua(lua_code).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
send_response(JsonRpcResponse {
|
send_text_result!(id.clone(), result);
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": result}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -800,15 +750,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
|
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
|
||||||
match execute_nvim_lua(code).await {
|
match execute_nvim_lua(code).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
send_response(JsonRpcResponse {
|
send_text_result!(id.clone(), result);
|
||||||
jsonrpc: "2.0".to_string(),
|
|
||||||
id,
|
|
||||||
result: Some(json!({
|
|
||||||
"content": [{"type": "text", "text": result}]
|
|
||||||
})),
|
|
||||||
error: None,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -915,3 +857,4 @@ mod tests {
|
|||||||
assert!(req.is_none());
|
assert!(req.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
start_pattern = r'#\[cfg\(windows\)\]\nasync fn call_nvim'
|
||||||
|
end_pattern = r'async fn send_nvim_command'
|
||||||
|
|
||||||
|
start_idx = re.search(start_pattern, content).start()
|
||||||
|
end_idx = re.search(end_pattern, content).start()
|
||||||
|
|
||||||
|
new_code = """use std::sync::LazyLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
pub struct NvimRequest {
|
||||||
|
pub msgid_str: String,
|
||||||
|
pub req: rmpv::Value,
|
||||||
|
pub reply: oneshot::Sender<Result<rmpv::Value, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static NVIM_CONN: LazyLock<Arc<Mutex<Option<mpsc::Sender<NvimRequest>>>>> = LazyLock::new(|| Arc::new(Mutex::new(None)));
|
||||||
|
|
||||||
|
async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
||||||
|
let mut conn_lock = NVIM_CONN.lock().await;
|
||||||
|
if let Some(sender) = conn_lock.as_ref() {
|
||||||
|
if !sender.is_closed() {
|
||||||
|
return Ok(sender.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Establishing new persistent connection to Neovim");
|
||||||
|
let socket_path = get_socket_path().await?;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
let stream = {
|
||||||
|
use tokio::net::windows::named_pipe::ClientOptions;
|
||||||
|
ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let stream = {
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut read_half, mut write_half) = tokio::io::split(stream);
|
||||||
|
let (tx, mut rx) = mpsc::channel::<NvimRequest>(32);
|
||||||
|
let pending_requests: Arc<Mutex<HashMap<String, oneshot::Sender<Result<rmpv::Value, String>>>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
// Write task
|
||||||
|
let pending_clone = Arc::clone(&pending_requests);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(req) = rx.recv().await {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
if let Err(e) = rmpv::encode::write_value(&mut buf, &req.req) {
|
||||||
|
let _ = req.reply.send(Err(e.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_clone.lock().await.insert(req.msgid_str.clone(), req.reply);
|
||||||
|
|
||||||
|
if write_half.write_all(&buf).await.is_err() {
|
||||||
|
tracing::error!("Failed to write to Neovim socket");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read task
|
||||||
|
let pending_clone2 = Arc::clone(&pending_requests);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut resp_buf = Vec::new();
|
||||||
|
let mut chunk = vec![0u8; 8192];
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut cursor = std::io::Cursor::new(&resp_buf[offset..]);
|
||||||
|
match rmpv::decode::read_value(&mut cursor) {
|
||||||
|
Ok(val) => {
|
||||||
|
offset += cursor.position() as usize;
|
||||||
|
|
||||||
|
if let rmpv::Value::Array(ref arr) = val {
|
||||||
|
if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) {
|
||||||
|
let msgid = &arr[1];
|
||||||
|
let msgid_str = format!("{:?}", msgid);
|
||||||
|
|
||||||
|
if let Some(reply_sender) = pending_clone2.lock().await.remove(&msgid_str) {
|
||||||
|
let _ = reply_sender.send(Ok(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim buffer if it gets too large
|
||||||
|
if offset > 1024 * 1024 {
|
||||||
|
resp_buf.drain(..offset);
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if offset > 0 {
|
||||||
|
resp_buf.drain(..offset);
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let read_future = read_half.read(&mut chunk);
|
||||||
|
match tokio::time::timeout(tokio::time::Duration::from_secs(60), read_future).await {
|
||||||
|
Ok(Ok(n)) if n > 0 => {
|
||||||
|
resp_buf.extend_from_slice(&chunk[..n]);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::error!("Neovim socket read loop closed or timeout");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup pending requests on disconnect
|
||||||
|
let mut pending = pending_clone2.lock().await;
|
||||||
|
for (_, sender) in pending.drain() {
|
||||||
|
let _ = sender.send(Err("Connection closed".to_string()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
*conn_lock = Some(tx.clone());
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
||||||
|
let msgid = if let rmpv::Value::Array(ref arr) = req {
|
||||||
|
if arr.len() > 1 {
|
||||||
|
arr[1].clone()
|
||||||
|
} else {
|
||||||
|
rmpv::Value::Nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rmpv::Value::Nil
|
||||||
|
};
|
||||||
|
|
||||||
|
let msgid_str = format!("{:?}", msgid);
|
||||||
|
let tx = get_nvim_connection().await?;
|
||||||
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
tx.send(NvimRequest {
|
||||||
|
msgid_str,
|
||||||
|
req,
|
||||||
|
reply: reply_tx,
|
||||||
|
}).await.map_err(|_| "Failed to send request to Neovim connection manager")?;
|
||||||
|
|
||||||
|
match tokio::time::timeout(tokio::time::Duration::from_secs(10), reply_rx).await {
|
||||||
|
Ok(Ok(res)) => res,
|
||||||
|
Ok(Err(_)) => Err("Response channel dropped".to_string()),
|
||||||
|
Err(_) => Err("Timeout waiting for Neovim response".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_content = content[:start_idx] + new_code + content[end_idx:]
|
||||||
|
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
print("Patched!")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
macro_def = """
|
||||||
|
macro_rules! send_text_result {
|
||||||
|
($id:expr, $text:expr) => {
|
||||||
|
send_response(JsonRpcResponse {
|
||||||
|
jsonrpc: "2.0".to_string(),
|
||||||
|
id: $id,
|
||||||
|
result: Some(json!({
|
||||||
|
"content": [{"type": "text", "text": $text}]
|
||||||
|
})),
|
||||||
|
error: None,
|
||||||
|
}).await;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if "macro_rules! send_text_result" not in content:
|
||||||
|
idx = content.find("pub async fn run_mcp_loop")
|
||||||
|
content = content[:idx] + macro_def + "\n" + content[idx:]
|
||||||
|
|
||||||
|
# Find tools/call block
|
||||||
|
tools_call_idx = content.find('"tools/call" => {')
|
||||||
|
if tools_call_idx != -1:
|
||||||
|
block = content[tools_call_idx:]
|
||||||
|
|
||||||
|
# Replace the verbose blocks
|
||||||
|
pattern = r'send_response\(JsonRpcResponse \{\s*jsonrpc: "2\.0"\.to_string\(\),\s*id,\s*result: Some\(json!\(\{\s*"content": \[\{"type": "text", "text": (.*?)\}\]\s*\}\)\),\s*error: None,\s*\}\)\s*\.await;'
|
||||||
|
|
||||||
|
new_block = re.sub(pattern, r'send_text_result!(id.clone(), \1);', block)
|
||||||
|
|
||||||
|
content = content[:tools_call_idx] + new_block
|
||||||
|
|
||||||
|
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\nvim-core\\src\\lib.rs', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
print("Patched nvim macro")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\stub\\src\\main.rs', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
start1 = r'let mut log_prefix = String::from\("Message"\);\n\s*if let Ok\(v\) = serde_json::from_str::<serde_json::Value>\(&msg\) \{[\s\S]*?log_prefix = format!\("Response id=\{\}", res_id\);\n\s*\}\n\s*\}'
|
||||||
|
content = re.sub(start1, r'let log_prefix = logger::extract_log_prefix(&msg, false);', content)
|
||||||
|
|
||||||
|
start2 = r'let mut log_prefix = String::from\("Message"\);\n\s*if let Ok\(v\) = serde_json::from_str::<serde_json::Value>\(&text\) \{[\s\S]*?log_prefix = format!\("Response id=\{\} \[Error: \{\}\]", id, is_error\);\n\s*\}'
|
||||||
|
content = re.sub(start2, r'let log_prefix = logger::extract_log_prefix(&text, true);', content)
|
||||||
|
|
||||||
|
with open('C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\stub\\src\\main.rs', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
print("Patched stub")
|
||||||
+56
-29
@@ -261,14 +261,33 @@ fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
.route("/gate/set", post(gate_set_handler))
|
.route("/gate/set", post(gate_set_handler))
|
||||||
.route(
|
.route(
|
||||||
"/shutdown",
|
"/shutdown",
|
||||||
post(|| async move {
|
post(
|
||||||
std::thread::spawn(|| {
|
|headers: axum::http::HeaderMap, State(state): State<Arc<AppState>>| async move {
|
||||||
tracing::info!("Received shutdown request via /shutdown endpoint. Exiting process cleanly.");
|
let token_path = state.handler.state.base_dir.join("admin.token");
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
let expected_token = std::fs::read_to_string(&token_path)
|
||||||
std::process::exit(0);
|
.unwrap_or_default()
|
||||||
});
|
.trim()
|
||||||
"Shutting down..."
|
.to_string();
|
||||||
}),
|
|
||||||
|
let auth_header = headers
|
||||||
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if expected_token.is_empty() || auth_header != format!("Bearer {}", expected_token) {
|
||||||
|
return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
tracing::info!(
|
||||||
|
"Received shutdown request via /shutdown endpoint. Exiting process cleanly."
|
||||||
|
);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
(axum::http::StatusCode::OK, "Shutting down...").into_response()
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/",
|
"/",
|
||||||
@@ -648,26 +667,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _guard = init_logging("mcp-memory-server");
|
let _guard = init_logging("mcp-memory-server");
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
if cli.exit {
|
if cli.exit {
|
||||||
let port = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
let port = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
let _ = std::process::Command::new("curl")
|
let token = std::fs::read_to_string(base.join("admin.token")).unwrap_or_default();
|
||||||
.arg("-k")
|
let mut cmd = std::process::Command::new("curl");
|
||||||
.arg("-X")
|
cmd.arg("-k").arg("-X").arg("POST");
|
||||||
.arg("POST")
|
if !token.is_empty() {
|
||||||
.arg(format!("https://127.0.0.1:{}/shutdown", port))
|
cmd.arg("-H").arg(format!("Authorization: Bearer {}", token.trim()));
|
||||||
.output();
|
}
|
||||||
|
let _ = cmd.arg(format!("https://127.0.0.1:{}/shutdown", port)).output();
|
||||||
println!("Sent shutdown request to server.");
|
println!("Sent shutdown request to server.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.restart {
|
if cli.restart {
|
||||||
let port = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
let port = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
let _ = std::process::Command::new("curl")
|
let token = std::fs::read_to_string(base.join("admin.token")).unwrap_or_default();
|
||||||
.arg("-k")
|
let mut cmd = std::process::Command::new("curl");
|
||||||
.arg("-X")
|
cmd.arg("-k").arg("-X").arg("POST");
|
||||||
.arg("POST")
|
if !token.is_empty() {
|
||||||
.arg(format!("https://127.0.0.1:{}/shutdown", port))
|
cmd.arg("-H").arg(format!("Authorization: Bearer {}", token.trim()));
|
||||||
.output();
|
}
|
||||||
|
let _ = cmd.arg(format!("https://127.0.0.1:{}/shutdown", port)).output();
|
||||||
println!("Sent shutdown request to existing server. Waiting for it to exit...");
|
println!("Sent shutdown request to existing server. Waiting for it to exit...");
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1500));
|
std::thread::sleep(std::time::Duration::from_millis(1500));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -691,18 +722,14 @@ 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");
|
fs::create_dir_all(&base).expect("Failed to create store dir");
|
||||||
|
|
||||||
|
// Generate token
|
||||||
|
let admin_token = uuid::Uuid::new_v4().to_string();
|
||||||
|
std::fs::write(base.join("admin.token"), &admin_token).expect("Failed to write admin token");
|
||||||
|
|
||||||
let redb_path = base.join("mcp_store.redb");
|
let redb_path = base.join("mcp_store.redb");
|
||||||
|
|
||||||
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
|
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
|
||||||
|
|
||||||
// Ensure table exists and migrate old JSON files
|
// Ensure table exists and migrate old JSON files
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ tracing = "0.1.44"
|
|||||||
tracing-subscriber = "0.3.23"
|
tracing-subscriber = "0.3.23"
|
||||||
dirs = "7.0.0"
|
dirs = "7.0.0"
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
|
mcp-stdio = { version = "0.1.0", path = "../mcp-stdio" }
|
||||||
|
regex = "1.13.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use std::sync::LazyLock;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
static ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#""id"\s*:\s*([^,}]+)"#).unwrap());
|
||||||
|
static METHOD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#""method"\s*:\s*"([^"]+)""#).unwrap());
|
||||||
|
static TOOL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#""name"\s*:\s*"([^"]+)""#).unwrap());
|
||||||
|
static ERROR_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#""error"\s*:\s*\{"#).unwrap());
|
||||||
|
static IS_ERROR_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#""isError"\s*:\s*true"#).unwrap());
|
||||||
|
|
||||||
|
pub fn extract_log_prefix(json_str: &str, is_response: bool) -> String {
|
||||||
|
let id = ID_REGEX.captures(json_str).and_then(|c| c.get(1)).map(|m| m.as_str()).unwrap_or("null");
|
||||||
|
|
||||||
|
if is_response {
|
||||||
|
let is_error = ERROR_REGEX.is_match(json_str) || IS_ERROR_REGEX.is_match(json_str);
|
||||||
|
return format!("Response id={} [Error: {}]", id, is_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let method = METHOD_REGEX.captures(json_str).and_then(|c| c.get(1)).map(|m| m.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
if method == "tools/call" {
|
||||||
|
let tool = TOOL_REGEX.captures(json_str).and_then(|c| c.get(1)).map(|m| m.as_str()).unwrap_or("unknown");
|
||||||
|
format!("ToolCall[{}] id={}", tool, id)
|
||||||
|
} else if !method.is_empty() {
|
||||||
|
format!("Request[{}] id={}", method, id)
|
||||||
|
} else if id != "null" {
|
||||||
|
format!("Response id={}", id)
|
||||||
|
} else {
|
||||||
|
"Message".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-53
@@ -12,40 +12,9 @@ struct Cli {
|
|||||||
target: String,
|
target: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_mcp_message(stdin: &mut tokio::io::BufReader<tokio::io::Stdin>) -> Option<String> {
|
|
||||||
use tokio::io::AsyncReadExt;
|
|
||||||
let mut length = 0;
|
|
||||||
loop {
|
|
||||||
let mut line = String::new();
|
|
||||||
let bytes_read = stdin.read_line(&mut line).await.unwrap_or(0);
|
|
||||||
if bytes_read == 0 {
|
|
||||||
tracing::info!("stdin EOF reached");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
tracing::info!("Read {} bytes from stdin: {:?}", bytes_read, line);
|
|
||||||
|
|
||||||
if line.starts_with('{') {
|
|
||||||
return Some(line.trim_end().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = line.trim_end();
|
mod logger;
|
||||||
if line.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let lower_line = line.to_lowercase();
|
|
||||||
if let Some(len_str) = lower_line.strip_prefix("content-length:") {
|
|
||||||
length = len_str.trim().parse().unwrap_or(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if length == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut buffer = vec![0; length];
|
|
||||||
if stdin.read_exact(&mut buffer).await.is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
String::from_utf8(buffer).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
|
fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
|
||||||
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("."));
|
||||||
@@ -74,13 +43,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
|
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
|
||||||
while let Some(msg) = read_mcp_message(&mut stdin).await {
|
while let Some(msg) = mcp_stdio::read_mcp_message(&mut stdin).await {
|
||||||
let _ = msg_tx.send(msg).await;
|
let _ = msg_tx.send(msg).await;
|
||||||
}
|
}
|
||||||
let _ = shutdown_tx.send(()).await;
|
let _ = shutdown_tx.send(()).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
let target_url = cli.target;
|
let target_url = if cli.target != "http://localhost:3000" {
|
||||||
|
cli.target
|
||||||
|
} else {
|
||||||
|
let port = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
|
format!("http://127.0.0.1:{}", port)
|
||||||
|
};
|
||||||
let ws_url = target_url.replace("http://", "ws://").replace("https://", "wss://");
|
let ws_url = target_url.replace("http://", "ws://").replace("https://", "wss://");
|
||||||
let ws_url = format!("{}/ws?client=proxy", ws_url);
|
let ws_url = format!("{}/ws?client=proxy", ws_url);
|
||||||
let msg_rx = Arc::new(tokio::sync::Mutex::new(msg_rx));
|
let msg_rx = Arc::new(tokio::sync::Mutex::new(msg_rx));
|
||||||
@@ -115,19 +89,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
match rx.recv().await {
|
match rx.recv().await {
|
||||||
Some(msg) => {
|
Some(msg) => {
|
||||||
drop(rx);
|
drop(rx);
|
||||||
let mut log_prefix = String::from("Message");
|
let log_prefix = logger::extract_log_prefix(&msg, false);
|
||||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg) {
|
|
||||||
let method = v.get("method").and_then(|m| m.as_str()).unwrap_or("");
|
|
||||||
let id = v.get("id").unwrap_or(&serde_json::Value::Null);
|
|
||||||
if method == "tools/call" {
|
|
||||||
let tool = v.get("params").and_then(|p| p.get("name")).and_then(|n| n.as_str()).unwrap_or("unknown");
|
|
||||||
log_prefix = format!("ToolCall[{}] id={}", tool, id);
|
|
||||||
} else if !method.is_empty() {
|
|
||||||
log_prefix = format!("Request[{}] id={}", method, id);
|
|
||||||
} else if let Some(res_id) = v.get("id") {
|
|
||||||
log_prefix = format!("Response id={}", res_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracing::info!(">>> [Stub] Forwarding {} to server (length: {}): {}", log_prefix, msg.len(), if msg.len() > 1000 { format!("{}...", &msg[..1000]) } else { msg.clone() });
|
tracing::info!(">>> [Stub] Forwarding {} to server (length: {}): {}", log_prefix, msg.len(), if msg.len() > 1000 { format!("{}...", &msg[..1000]) } else { msg.clone() });
|
||||||
if write.send(tokio_tungstenite::tungstenite::Message::Text(msg)).await.is_err() {
|
if write.send(tokio_tungstenite::tungstenite::Message::Text(msg)).await.is_err() {
|
||||||
|
|
||||||
@@ -143,12 +105,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut recv_task = tokio::spawn(async move {
|
let mut recv_task = tokio::spawn(async move {
|
||||||
while let Some(Ok(msg)) = read.next().await {
|
while let Some(Ok(msg)) = read.next().await {
|
||||||
if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
|
if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
|
||||||
let mut log_prefix = String::from("Message");
|
let log_prefix = logger::extract_log_prefix(&text, true);
|
||||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
|
|
||||||
let id = v.get("id").unwrap_or(&serde_json::Value::Null);
|
|
||||||
let is_error = v.get("error").is_some() || v.get("result").and_then(|res| res.get("isError")).and_then(|e| e.as_bool()).unwrap_or(false);
|
|
||||||
log_prefix = format!("Response id={} [Error: {}]", id, is_error);
|
|
||||||
}
|
|
||||||
tracing::info!("<<< [Stub] Received {} from server (length: {}): {}", log_prefix, text.len(), if text.len() > 1000 { format!("{}...", &text[..1000]) } else { text.clone() });
|
tracing::info!("<<< [Stub] Received {} from server (length: {}): {}", log_prefix, text.len(), if text.len() > 1000 { format!("{}...", &text[..1000]) } else { text.clone() });
|
||||||
let payload = format!("{}\n", text);
|
let payload = format!("{}\n", text);
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
@@ -185,3 +142,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user