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:
1 parent
28f70aafdb
commit
67b6a0407e
14 files changed
+1271
-19
No files matched your search
@@ -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())
|
||||
}
|
||||
Reference in new issue
Block a user