feat: Dual-OS Neovim MCP Integration

- Created linux-nvim and win-nvim MCP servers for deep Neovim Msgpack-RPC integration.
- Implemented tools: nvim_goto_line, nvim_get_active_buffer, nvim_get_cursor, nvim_get_visual_selection, nvim_set_diagnostics.
- Added gemini-integration.lua for active UI socket discovery.
- Updated build.cmd to cross-compile for both Windows (gnu) and Linux (musl).
- Updated README, design.md, and instructions.md with architecture and tool capabilities.
This commit is contained in:
Reazul Ashraf committed 2026-09-10 23:20:03 +01:00
1 parent 28f70aafdb
commit 67b6a0407e
14 files changed
+1271 -19

No files matched your search

Generated
+90 -3
View File
@@ -1031,6 +1031,18 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "mcp-memory-linux-nvim"
version = "0.1.0"
dependencies = [
"rmp-serde",
"rmpv",
"schemars 0.8.22",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "mcp-memory-server"
version = "0.1.0"
@@ -1045,7 +1057,7 @@ dependencies = [
"glob",
"redb",
"reqwest",
"schemars",
"schemars 1.2.2",
"serde",
"serde_json",
"tantivy",
@@ -1068,6 +1080,18 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "mcp-memory-win-nvim"
version = "0.1.0"
dependencies = [
"rmp-serde",
"rmpv",
"schemars 0.8.22",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "measure_time"
version = "0.9.0"
@@ -1522,6 +1546,34 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]]
name = "rmpv"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417"
dependencies = [
"rmp",
]
[[package]]
name = "rust-stemmers"
version = "1.2.0"
@@ -1598,6 +1650,18 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "schemars"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"dyn-clone",
"schemars_derive 0.8.22",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.2.2"
@@ -1606,11 +1670,23 @@ checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"schemars_derive 1.2.2",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals 0.29.1",
"syn 2.0.119",
]
[[package]]
name = "schemars_derive"
version = "1.2.2"
@@ -1619,7 +1695,7 @@ checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"serde_derive_internals 0.30.0",
"syn 3.0.5",
]
@@ -1659,6 +1735,17 @@ dependencies = [
"syn 3.0.5",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "serde_derive_internals"
version = "0.30.0"
+3 -1
View File
@@ -1,6 +1,8 @@
[workspace]
members = [
"server",
"stub"
"stub",
"win-nvim",
"linux-nvim"
]
resolver = "2"
+15
View File
@@ -82,3 +82,18 @@ You can also programmatically query these live metrics via the API endpoint:
## Further Reading
For a deep dive into the architecture, Write-Ahead Logging (WAL), locking mechanisms, and the HTTP SSE event loop, consult the design.md file in this repository.
## Neovim Integration
The `linux-nvim` and `win-nvim` MCP servers provide direct Msgpack-RPC communication with Neovim.
For this to work, you must add the provided `gemini-integration.lua` file to your Neovim `lua/` configuration directory and require it in your `init.lua`:
```lua
require('gemini-integration')
```
This script allows Neovim to broadcast its active socket to `~/.gemini/active_nvim.txt` when it gains focus, allowing the MCP servers to bridge the connection.
These MCP servers provide the following two-way integration tools:
* **`nvim_goto_line`**: Open files and jump cursors directly from the LLM.
* **`nvim_set_diagnostics`**: Push inline code review warnings as virtual text.
* **`nvim_get_active_buffer`**: Read live, unsaved buffer contents.
* **`nvim_get_cursor`**: Fetch precise line/column coordinates.
* **`nvim_get_visual_selection`**: Read highlighted code blocks.
+44 -9
View File
@@ -1,12 +1,47 @@
@echo off
echo Building mcp-memory in release mode...
cargo build --release
setlocal
if %ERRORLEVEL% EQU 0 (
echo.
echo Build successful!
echo Executable is located at: target\release\mcp-memory.exe
) else (
echo.
echo Build failed with error code %ERRORLEVEL%.
echo Building mcp-memory (server and stub) for Windows...
rustup run stable cargo build --release
if %ERRORLEVEL% NEQ 0 (
echo Windows build failed!
exit /b %ERRORLEVEL%
)
echo Building mcp-memory (server and stub) for Linux (WSL)...
rustup run stable cargo zigbuild --release --target x86_64-unknown-linux-musl
if %ERRORLEVEL% NEQ 0 (
echo Linux build failed!
exit /b %ERRORLEVEL%
)
echo.
echo Build successful! Gracefully shutting down running servers...
:: Attempt shutdown via CLI flag
if exist "C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe" (
"C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe" --exit >nul 2>&1
)
:: Fallback shutdown via HTTP endpoint
curl -X POST http://127.0.0.1:3000/shutdown -s -o nul >nul 2>&1
:: Give the server a moment to flush checkpoints and release file locks
timeout /t 2 /nobreak >nul
echo.
echo Copying Windows executables...
copy /Y target\release\mcp-memory-server.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe"
copy /Y target\release\mcp-memory-stub.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-stub.exe"
copy /Y target\x86_64-pc-windows-gnu\release\mcp-memory-win-nvim.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-win-nvim.exe"
echo Copying Linux executables to WSL...
wsl -d Ubuntu -e bash -c "cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/target/x86_64-unknown-linux-musl/release/mcp-memory-server /home/riz/.local/bin/mcp-memory-server && chmod +x /home/riz/.local/bin/mcp-memory-server"
wsl -d Ubuntu -e bash -c "cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/target/x86_64-unknown-linux-musl/release/mcp-memory-stub /home/riz/.local/bin/mcp-memory-stub && chmod +x /home/riz/.local/bin/mcp-memory-stub"
wsl -d Ubuntu -e bash -c "cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/target/x86_64-unknown-linux-musl/release/mcp-memory-linux-nvim /home/riz/.local/bin/mcp-memory-linux-nvim && chmod +x /home/riz/.local/bin/mcp-memory-linux-nvim"
echo Copying instructions.md...
copy /Y instructions.md "C:\Users\reazul.ashraf\.gemini\antigravity-cli\mcp\memory\instructions.md"
wsl -d Ubuntu -e bash -c "mkdir -p /home/riz/.gemini/antigravity-cli/mcp/memory && cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/instructions.md /home/riz/.gemini/antigravity-cli/mcp/memory/instructions.md"
echo.
echo Build and deployment complete!
endlocal
+1
View File
@@ -114,3 +114,4 @@ To optimize for different environments, the codebase is structured as a Cargo Wo
eqwest and okio).
* **Role:** A dedicated, OS-agnostic proxy binary used strictly for routing stdio JSON-RPC traffic over HTTP to a remote Leader. Windows gy clients point directly to this binary to bypass loading the heavy Server daemon into memory.
* **WSL Behavior:** Compiled as a Linux native binary (x86_64-unknown-linux-musl). When executed by WSL agy, it acts as a transparent proxy to http://127.0.0.1:3000. It can also execute wake_cmd (e.g., WSL interop) to silently wake the Windows host if the Leader is offline.
+26
View File
@@ -0,0 +1,26 @@
-- gemini-integration.lua
-- Registers the active Neovim instance for the Antigravity MCP Server
vim.api.nvim_create_autocmd({"VimEnter", "FocusGained"}, {
callback = function()
-- Only register if Neovim has an active UI attached (ignore headless/embedded instances)
if #vim.api.nvim_list_uis() > 0 then
local server_name = vim.v.servername
if server_name then
local home = os.getenv("HOME")
if not home then
home = os.getenv("USERPROFILE")
end
if home then
os.execute("mkdir -p " .. home .. "/.gemini")
local path = home .. "/.gemini/active_nvim.txt"
local f = io.open(path, "w")
if f then
f:write(server_name)
f:close()
end
end
end
end
end,
desc = "Register active Neovim instance for Gemini CLI",
})
+7
View File
@@ -131,3 +131,10 @@ When starting a new session, get a numerical aggregate of the project's current
To maintain absolute traceability, we link memory items directly to the exact git commits they occurred on.
- When calling **log_code_change**, **log_error_fix**, or **log_tech_debt**, you should execute git rev-parse HEAD and git branch --show-current in the project directory first (if it's a git repo).
- Pass the resulting hash and branch name into the git_commit and git_branch arguments of those tools to permanently link the memory item to the VCS state.
## 13. Server Lifecycle & Updates (Graceful Shutdown)
When instructed to restart, update, or stop the mcp-memory-server binary, never use brute-force OS kill commands (e.g. Stop-Process, pkill, kill).
- Why: The server uses asynchronous checkpointing. Force-killing it bypasses the shutdown flush, risking data loss in the master JSON graph, and can lead to lingering file locks.
- How: Always use the built-in graceful shutdown mechanisms:
1. CLI Flag: mcp-memory-server --exit (or --restart)
2. HTTP Endpoint: POST http://127.0.0.1:3000/shutdown
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "mcp-memory-linux-nvim"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rmp-serde = "1.1"
rmpv = "1.0"
tokio = { version = "1.37", features = ["full"] }
schemars = "0.8"
+469
View File
@@ -0,0 +1,469 @@
mod mcp;
use mcp::{read_message, send_response, send_error, JsonRpcResponse};
use serde_json::{json, Value};
use tokio::net::UnixStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() {
loop {
let msg = match read_message() {
Some(m) => m,
None => break,
};
let id = msg.id.unwrap_or(json!(null));
match msg.method.as_str() {
"initialize" => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"capabilities": {},
"serverInfo": {
"name": "mcp-memory-linux-nvim",
"version": "0.1.0"
}
})),
error: None,
});
}
"tools/list" => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"tools": [
{
"name": "nvim_goto_line",
"description": "Open a file and jump to a specific line",
"inputSchema": {
"type": "object",
"properties": {
"file": { "type": "string" },
"line": { "type": "integer" }
},
"required": ["file", "line"]
}
},
{
"name": "nvim_get_active_buffer",
"description": "Get the contents of the currently active Neovim buffer",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_get_cursor",
"description": "Get the current cursor position (line and column) in the active Neovim buffer",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_get_visual_selection",
"description": "Get the text that is currently highlighted or was last highlighted in Visual mode",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_set_diagnostics",
"description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer",
"inputSchema": {
"type": "object",
"properties": {
"line": { "type": "integer" },
"message": { "type": "string" }
},
"required": ["line", "message"]
}
}
]
})),
error: None,
});
}
"tools/call" => {
let params = msg.params.unwrap_or(json!({}));
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or(json!({}));
match name {
"nvim_goto_line" => {
let file = args.get("file").and_then(|v| v.as_str()).unwrap_or("");
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
match send_nvim_command(&format!("edit +{} {}", line, file)).await {
Ok(_) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_get_active_buffer" => {
match get_nvim_active_buffer().await {
Ok(content) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": content}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_get_cursor" => {
match get_nvim_cursor().await {
Ok(content) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": content}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_get_visual_selection" => {
match get_nvim_visual_selection().await {
Ok(content) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": content}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_set_diagnostics" => {
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
let message = args.get("message").and_then(|v| v.as_str()).unwrap_or("");
match set_nvim_diagnostics(line, message).await {
Ok(_) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
_ => {
send_error(id, -32601, "Tool not found");
}
}
}
_ => {
// Ignore other methods
}
}
}
}
async fn get_socket_path() -> Result<String, String> {
if let Ok(content) = std::fs::read_to_string(format!("{}/.gemini/active_nvim.txt", std::env::var("HOME").unwrap_or("/home/riz".into()))) {
let p = content.trim().to_string();
if !p.is_empty() {
return Ok(p);
}
}
// Fallback: search /tmp/nvim.*/0
if let Ok(entries) = std::fs::read_dir("/tmp") {
for entry in entries.flatten() {
if let Ok(name) = entry.file_name().into_string() {
if name.starts_with("nvim.") {
let path = entry.path().join("0");
if path.exists() {
return Ok(path.to_string_lossy().to_string());
}
}
}
}
}
Err("Could not find Neovim socket".to_string())
}
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
let socket_path = get_socket_path().await?;
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(1.into()),
RmpValue::String("nvim_command".into()),
RmpValue::Array(vec![RmpValue::String(cmd.into())]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
Ok(())
}
async fn get_nvim_active_buffer() -> Result<String, String> {
let socket_path = get_socket_path().await?;
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(2.into()),
RmpValue::String("nvim_buf_get_lines".into()),
RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(0.into()),
RmpValue::Integer((-1).into()),
RmpValue::Boolean(true),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 1024 * 1024];
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
if let RmpValue::Array(lines) = &arr[3] {
let mut text = String::new();
for line in lines {
if let RmpValue::String(s) = line {
if let Some(s) = s.as_str() {
text.push_str(s);
text.push('\n');
}
}
}
return Ok(text);
}
}
}
Err("Invalid response from Neovim".to_string())
}
async fn get_nvim_cursor() -> Result<String, String> {
let socket_path = get_socket_path().await?;
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(2.into()),
RmpValue::String("nvim_win_get_cursor".into()),
RmpValue::Array(vec![
RmpValue::Integer(0.into()),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 8192];
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
if let RmpValue::Array(pos) = &arr[3] {
if pos.len() == 2 {
if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) {
return Ok(format!("Line: {}, Column: {}", row, col));
}
}
}
}
}
Err("Invalid response from Neovim".to_string())
}
async fn get_nvim_visual_selection() -> Result<String, String> {
let socket_path = get_socket_path().await?;
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
let lua_script = r#"
local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<"))
local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>"))
local lines = vim.fn.getline(csrow, cerow)
if type(lines) == "table" then
return table.concat(lines, "\n")
else
return lines
end
"#;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(3.into()),
RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![
RmpValue::String(lua_script.into()),
RmpValue::Array(vec![]),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 1024 * 1024];
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
if let RmpValue::String(s) = &arr[3] {
if let Some(text) = s.as_str() {
return Ok(text.to_string());
}
}
}
}
Err("Invalid response from Neovim".to_string())
}
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
let socket_path = get_socket_path().await?;
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
// We escape backslashes and double quotes in the message
let escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\"");
let lua_script = format!(r#"
local ns = vim.api.nvim_create_namespace("gemini_diagnostics")
local diagnostics = {{{{
lnum = {} - 1,
col = 0,
severity = vim.diagnostic.severity.WARN,
message = "{}",
}}}}
vim.diagnostic.set(ns, 0, diagnostics, {{}})
"#, line, escaped_message);
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(4.into()),
RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![
RmpValue::String(lua_script.into()),
RmpValue::Array(vec![]),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 8192];
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
return Ok(());
}
}
Err("Invalid response from Neovim".to_string())
}
+62
View File
@@ -0,0 +1,62 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: Option<Value>,
pub method: String,
pub params: Option<Value>,
}
#[derive(Serialize, Debug, Clone)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<Value>,
}
pub fn read_message() -> Option<JsonRpcRequest> {
use std::io::{BufRead, Read};
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut length = 0;
loop {
let mut line = String::new();
if handle.read_line(&mut line).unwrap_or(0) == 0 {
return None;
}
let line = line.trim_end();
if line.is_empty() {
break;
}
if let Some(len_str) = line.strip_prefix("Content-Length: ") {
length = len_str.parse().unwrap_or(0);
}
}
if length == 0 {
return None;
}
let mut buffer = vec![0; length];
handle.read_exact(&mut buffer).unwrap_or(());
serde_json::from_slice(&buffer).ok()
}
pub fn send_response(response: JsonRpcResponse) {
let msg = serde_json::to_string(&response).unwrap();
print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
use std::io::Write;
std::io::stdout().flush().unwrap();
}
pub fn send_error(id: Value, code: i32, message: &str) {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(serde_json::json!({"code": code, "message": message})),
});
}
+8 -6
View File
@@ -7,10 +7,12 @@ use tokio::sync::mpsc;
use tokio_util::io::StreamReader;
#[derive(Parser)]
#[command(name = "mcp-memory-stub")]
#[command(name = "mcp-memory-stub", author, version, about = "Antigravity MCP Memory Stub / Proxy", long_about = None)]
struct Cli {
/// Target URL for the stub to proxy messages to
#[arg(long, default_value = "http://localhost:3000")]
target: String,
/// Optional command to execute if the target server is unreachable
#[arg(long)]
wake_cmd: Option<String>,
}
@@ -56,11 +58,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.send()
.await;
if res.is_ok() {
break;
}
if let Ok(resp) = res { if resp.status().is_success() { break; } }
}
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
attempts += 1;
if attempts % 10 == 0 {
eprintln!("[PROXY] Waiting for server to accept messages...");
@@ -132,7 +132,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
*post_url.write().await = String::new();
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
Err(_) => {
if let Some(ref cmd) = wake_cmd {
@@ -150,3 +150,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
})
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "mcp-memory-win-nvim"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rmp-serde = "1.1"
rmpv = "1.0"
tokio = { version = "1.37", features = ["full"] }
schemars = "0.8"
+460
View File
@@ -0,0 +1,460 @@
mod mcp;
use mcp::{read_message, send_response, send_error, JsonRpcResponse};
use serde_json::{json, Value};
use tokio::net::windows::named_pipe::ClientOptions;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() {
loop {
let msg = match read_message() {
Some(m) => m,
None => break,
};
let id = msg.id.unwrap_or(json!(null));
match msg.method.as_str() {
"initialize" => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"capabilities": {},
"serverInfo": {
"name": "mcp-memory-win-nvim",
"version": "0.1.0"
}
})),
error: None,
});
}
"tools/list" => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"tools": [
{
"name": "nvim_goto_line",
"description": "Open a file and jump to a specific line",
"inputSchema": {
"type": "object",
"properties": {
"file": { "type": "string" },
"line": { "type": "integer" }
},
"required": ["file", "line"]
}
},
{
"name": "nvim_get_active_buffer",
"description": "Get the contents of the currently active Neovim buffer",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_get_cursor",
"description": "Get the current cursor position (line and column) in the active Neovim buffer",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_get_visual_selection",
"description": "Get the text that is currently highlighted or was last highlighted in Visual mode",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_set_diagnostics",
"description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer",
"inputSchema": {
"type": "object",
"properties": {
"line": { "type": "integer" },
"message": { "type": "string" }
},
"required": ["line", "message"]
}
}
]
})),
error: None,
});
}
"tools/call" => {
let params = msg.params.unwrap_or(json!({}));
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or(json!({}));
match name {
"nvim_goto_line" => {
let file = args.get("file").and_then(|v| v.as_str()).unwrap_or("");
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
match send_nvim_command(&format!("edit +{} {}", line, file)).await {
Ok(_) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_get_active_buffer" => {
match get_nvim_active_buffer().await {
Ok(content) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": content}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_get_cursor" => {
match get_nvim_cursor().await {
Ok(content) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": content}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_get_visual_selection" => {
match get_nvim_visual_selection().await {
Ok(content) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": content}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
"nvim_set_diagnostics" => {
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
let message = args.get("message").and_then(|v| v.as_str()).unwrap_or("");
match set_nvim_diagnostics(line, message).await {
Ok(_) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}]
})),
error: None,
});
}
Err(e) => {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: Some(json!({
"isError": true,
"content": [{"type": "text", "text": format!("Error: {}", e)}]
})),
error: None,
});
}
}
}
_ => {
send_error(id, -32601, "Tool not found");
}
}
}
_ => {
// Ignore other methods
}
}
}
}
async fn get_socket_path() -> Result<String, String> {
let profile = std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
let path = format!("{}\\.gemini\\active_nvim.txt", profile);
if let Ok(content) = std::fs::read_to_string(&path) {
let p = content.trim().to_string();
if !p.is_empty() {
return Ok(p);
}
}
Err("Could not find active Windows Neovim named pipe in active_nvim.txt".to_string())
}
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
let socket_path = get_socket_path().await?;
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(1.into()),
RmpValue::String("nvim_command".into()),
RmpValue::Array(vec![RmpValue::String(cmd.into())]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
client.write_all(&buf).await.map_err(|e| e.to_string())?;
Ok(())
}
async fn get_nvim_active_buffer() -> Result<String, String> {
let socket_path = get_socket_path().await?;
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(2.into()),
RmpValue::String("nvim_buf_get_lines".into()),
RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(0.into()),
RmpValue::Integer((-1).into()),
RmpValue::Boolean(true),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
client.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 1024 * 1024]; // 1MB buffer
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
if let RmpValue::Array(lines) = &arr[3] {
let mut text = String::new();
for line in lines {
if let RmpValue::String(s) = line {
if let Some(s) = s.as_str() {
text.push_str(s);
text.push('\n');
}
}
}
return Ok(text);
}
}
}
Err("Invalid response from Neovim".to_string())
}
async fn get_nvim_cursor() -> Result<String, String> {
let socket_path = get_socket_path().await?;
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(2.into()),
RmpValue::String("nvim_win_get_cursor".into()),
RmpValue::Array(vec![
RmpValue::Integer(0.into()),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
client.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 8192];
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
if let RmpValue::Array(pos) = &arr[3] {
if pos.len() == 2 {
if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) {
return Ok(format!("Line: {}, Column: {}", row, col));
}
}
}
}
}
Err("Invalid response from Neovim".to_string())
}
async fn get_nvim_visual_selection() -> Result<String, String> {
let socket_path = get_socket_path().await?;
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
let lua_script = r#"
local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<"))
local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>"))
local lines = vim.fn.getline(csrow, cerow)
if type(lines) == "table" then
return table.concat(lines, "\n")
else
return lines
end
"#;
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(3.into()),
RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![
RmpValue::String(lua_script.into()),
RmpValue::Array(vec![]),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
client.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 1024 * 1024];
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
if let RmpValue::String(s) = &arr[3] {
if let Some(text) = s.as_str() {
return Ok(text.to_string());
}
}
}
}
Err("Invalid response from Neovim".to_string())
}
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
let socket_path = get_socket_path().await?;
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
let escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\"");
let lua_script = format!(r#"
local ns = vim.api.nvim_create_namespace("gemini_diagnostics")
local diagnostics = {{{{
lnum = {} - 1,
col = 0,
severity = vim.diagnostic.severity.WARN,
message = "{}",
}}}}
vim.diagnostic.set(ns, 0, diagnostics, {{}})
"#, line, escaped_message);
use rmpv::Value as RmpValue;
let req = RmpValue::Array(vec![
RmpValue::Integer(0.into()),
RmpValue::Integer(4.into()),
RmpValue::String("nvim_exec_lua".into()),
RmpValue::Array(vec![
RmpValue::String(lua_script.into()),
RmpValue::Array(vec![]),
]),
]);
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
client.write_all(&buf).await.map_err(|e| e.to_string())?;
let mut resp_buf = [0u8; 8192];
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
if let RmpValue::Array(arr) = resp {
if arr.len() >= 4 {
if !arr[2].is_nil() {
return Err(format!("Neovim error: {:?}", arr[2]));
}
return Ok(());
}
}
Err("Invalid response from Neovim".to_string())
}
+62
View File
@@ -0,0 +1,62 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: Option<Value>,
pub method: String,
pub params: Option<Value>,
}
#[derive(Serialize, Debug, Clone)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<Value>,
}
pub fn read_message() -> Option<JsonRpcRequest> {
use std::io::{BufRead, Read};
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut length = 0;
loop {
let mut line = String::new();
if handle.read_line(&mut line).unwrap_or(0) == 0 {
return None;
}
let line = line.trim_end();
if line.is_empty() {
break;
}
if let Some(len_str) = line.strip_prefix("Content-Length: ") {
length = len_str.parse().unwrap_or(0);
}
}
if length == 0 {
return None;
}
let mut buffer = vec![0; length];
handle.read_exact(&mut buffer).unwrap_or(());
serde_json::from_slice(&buffer).ok()
}
pub fn send_response(response: JsonRpcResponse) {
let msg = serde_json::to_string(&response).unwrap();
print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
use std::io::Write;
std::io::stdout().flush().unwrap();
}
pub fn send_error(id: Value, code: i32, message: &str) {
send_response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(serde_json::json!({"code": code, "message": message})),
});
}