feat(mcp): Add 5 developer enhancements (git, logs, clipboard watcher, ast skeleton, nvim ghost text)

This commit is contained in:
Riz Ashraf committed 2026-09-30 05:29:50 +01:00
1 parent ccc268d807
commit 0e9748f797
27 files changed
+1401 -174

No files matched your search

+17
View File
@@ -24,6 +24,15 @@ rmcp = { version = "3.4.0", features = ["server"] }
thiserror = "2.0.20"
tower = { version = "0.5.3", features = ["util"] }
fastembed = "7.1.0"
arboard = "3.4.0"
image = "0.25.0"
base64 = "0.23.1"
clipboard-win = "5.4.1"
git2 = "0.19.0"
tree-sitter = "0.23.0"
tree-sitter-rust = "0.23.0"
tree-sitter-typescript = "0.23.0"
tree-sitter-python = "0.23.0"
[build-dependencies]
chrono = "0.4.45"
@@ -31,3 +40,11 @@ winres = "0.1.12"
[dev-dependencies]
tempfile = "3.27.0"
[[bin]]
name = "bin_clipboard_test"
path = "src/bin_clipboard_test.rs"
[[bin]]
name = "test_clip_write"
path = "src/test_clip_write.rs"
+50 -7
View File
@@ -23,6 +23,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
)
.route("/ws", get(ws_handler))
.route("/health", get(health_handler))
.route("/ping", get(|| async move { (axum::http::StatusCode::OK, "pong") }))
.route("/nvim/telemetry", post(nvim_telemetry_handler))
.route("/api/terminal/history", get(crate::api::telemetry::get_terminal_history_handler))
.route("/terminal/telemetry", post(crate::api::telemetry::terminal_telemetry_handler))
@@ -50,13 +51,12 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
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);
});
tracing::info!(
"Received shutdown request via /shutdown endpoint. Initiating graceful shutdown."
);
if let Some(tx) = state.shutdown_tx.lock().unwrap().take() {
let _ = tx.send(());
}
(axum::http::StatusCode::OK, "Shutting down...").into_response()
},
),
@@ -279,6 +279,48 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
}
}),
)
.route(
"/api/clipboard/capture",
post({
move || async move {
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
let mut clipboard = arboard::Clipboard::new()
.map_err(|e| format!("Failed to access clipboard: {}", e))?;
let image_data = clipboard.get_image()
.map_err(|e| format!("No image found in clipboard (or unsupported format): {}", e))?;
let img: image::RgbaImage = image::ImageBuffer::from_raw(
image_data.width as u32,
image_data.height as u32,
image_data.bytes.into_owned(),
)
.ok_or("Failed to parse raw clipboard image data")?;
let mut dynamic_img = image::DynamicImage::ImageRgba8(img);
let max_dim = 1024;
if dynamic_img.width() > max_dim || dynamic_img.height() > max_dim {
dynamic_img = dynamic_img.resize(max_dim, max_dim, image::imageops::FilterType::Lanczos3);
}
let rgb_img = dynamic_img.into_rgb8();
use base64::{Engine as _, engine::general_purpose::STANDARD};
let mut cursor = std::io::Cursor::new(Vec::new());
rgb_img.write_to(&mut cursor, image::ImageFormat::Jpeg).map_err(|e| e.to_string())?;
let b64 = STANDARD.encode(cursor.into_inner());
Ok(b64)
})
.await
.map_err(|e| format!("Task panic: {}", e))
.and_then(|r| r);
match result {
Ok(b64) => axum::Json(serde_json::json!({ "success": true, "image": format!("data:image/jpeg;base64,{}", b64) })),
Err(e) => axum::Json(serde_json::json!({ "success": false, "error": e }))
}
}
})
)
.with_state(app_state)
}
@@ -318,3 +360,4 @@ mod tests {
assert_eq!(response.status(), 200);
}
}
// Force recompile
+12
View File
@@ -0,0 +1,12 @@
use clipboard_win::{formats, get_clipboard, Clipboard};
fn main() {
if let Ok(_clip) = Clipboard::new_attempts(10) {
let text: Result<String, _> = get_clipboard(formats::Unicode);
let files: Result<Vec<String>, _> = get_clipboard(formats::FileList);
println!("Text: {:?}", text.ok());
println!("Files: {:?}", files.ok());
} else {
println!("Failed to open clipboard");
}
}
+47
View File
@@ -0,0 +1,47 @@
use crate::state::MemoryState;
use crate::models::StickyNote;
use std::sync::Arc;
use tokio::time::{sleep, Duration};
use clipboard_win::{formats, get_clipboard, Clipboard};
pub fn spawn_watcher(state: Arc<MemoryState>) {
tokio::spawn(async move {
let mut last_text = String::new();
loop {
sleep(Duration::from_millis(1000)).await;
let is_enabled = {
let watch = state.clipboard_watch_mode.read().await;
*watch
};
if !is_enabled {
continue;
}
if let Ok(_clip) = tokio::task::spawn_blocking(|| Clipboard::new_attempts(3)).await.unwrap() {
if let Ok(text) = get_clipboard::<String, _>(formats::Unicode) {
if text != last_text && !text.trim().is_empty() {
last_text = text.clone();
let note = StickyNote {
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
content: text,
expires_at: None,
};
state.sticky.modify(|notes| {
notes.push(note.clone());
});
// We use rebuild_index to index the new sticky note
state.rebuild_index().await;
}
}
}
}
});
}
+33
View File
@@ -473,11 +473,24 @@
<span id="app-version" style="font-size:0.8em; color:var(--text-secondary); margin-left:12px; font-family:monospace;"></span>
</div>
<div class="header-actions">
<button class="theme-toggle" onclick="testClipboard()" title="Test Clipboard Vision Tool" style="background:#3498db; color:white; border-radius:4px; padding:4px 8px; font-size:14px; display:flex; align-items:center; gap:5px;">📋 Inspect Clipboard</button>
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle Dark Mode">🌓</button>
<div style="font-weight: 500;"><span class="status-dot"></span>System Online</div>
</div>
</div>
<!-- Clipboard Modal -->
<div id="clipboard-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:9999; justify-content:center; align-items:center; flex-direction:column;">
<div style="background:var(--card-bg); padding:20px; border-radius:8px; max-width:80%; max-height:80%; display:flex; flex-direction:column; gap:15px; border:1px solid var(--border-color); box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
<div style="display:flex; justify-content:space-between; align-items:center;">
<h3 style="margin:0;">Clipboard Vision Extraction</h3>
<button onclick="document.getElementById('clipboard-modal').style.display='none'" style="background:none; border:none; color:var(--text-primary); font-size:20px; cursor:pointer;">&times;</button>
</div>
<div id="clipboard-result" style="overflow:auto; text-align:center; display:flex; justify-content:center;">
<p id="clipboard-loading">Processing clipboard...</p>
</div>
</div>
</div>
<div class="main-container">
<div class="tabs">
<button class="tab-button active" onclick="switchTab('graph-tab', this)">Graph Explorer</button>
@@ -1192,6 +1205,26 @@
`);
}
async function testClipboard() {
const modal = document.getElementById('clipboard-modal');
const resultDiv = document.getElementById('clipboard-result');
modal.style.display = 'flex';
resultDiv.innerHTML = '<p id="clipboard-loading" style="color:var(--text-secondary);">Analyzing your clipboard...</p>';
try {
const res = await fetch('/api/clipboard/capture', { method: 'POST' });
const data = await res.json();
if (data.success) {
resultDiv.innerHTML = `<img src="${data.image}" style="max-width:100%; max-height:60vh; border-radius:4px; border:1px solid var(--border-color); box-shadow: 0 5px 15px rgba(0,0,0,0.3);" />`;
} else {
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Failed to read clipboard: ${data.error}</p>`;
}
} catch (err) {
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Error calling endpoint: ${err.message}</p>`;
}
}
async function loadVersion() {
try {
const res = await fetch('/api/version');
+9 -1
View File
@@ -5,7 +5,15 @@ use std::sync::Arc;
pub fn init_redb(base: &Path) -> Arc<Database> {
let redb_path = base.join("mcp_store.redb");
let db = Arc::new(redb::Database::create(&redb_path).expect("Failed to create redb database"));
let db = match redb::Database::create(&redb_path) {
Ok(db) => Arc::new(db),
Err(e) => {
let err_msg = format!("CRITICAL FATAL ERROR: Failed to open or create redb database at {:?}. Error: {}. This almost always means another instance of mcp-memory-server is already running and holds the exclusive file lock on the database.", redb_path, e);
eprintln!("{}", err_msg);
tracing::error!("{}", err_msg);
std::process::exit(1);
}
};
// Ensure the table exists and migrate legacy JSON files
let write_txn = db.begin_write().expect("Failed to begin write txn on redb");
+106
View File
@@ -0,0 +1,106 @@
use crate::router::McpTool;
use crate::state::MemoryState;
use crate::tools::ReadFileSkeletonTool;
use async_trait::async_trait;
use serde_json::{json, Value};
use std::sync::Arc;
use tree_sitter::{Parser, Node};
pub struct ReadFileSkeletonHandler;
#[async_trait]
impl McpTool for ReadFileSkeletonHandler {
fn name(&self) -> &'static str {
"read_file_skeleton"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ReadFileSkeletonTool>(
"read_file_skeleton",
"Read a source file and return only its AST structural skeleton, omitting implementation details to save tokens.",
)
}
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
let tool_args: ReadFileSkeletonTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let file_path = tool_args.file_path.clone();
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
let code = std::fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file: {}", e))?;
let mut parser = Parser::new();
let ext = std::path::Path::new(&file_path)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("");
let language = match ext {
"rs" => tree_sitter_rust::LANGUAGE,
"ts" | "tsx" | "js" | "jsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
"py" => tree_sitter_python::LANGUAGE,
_ => return Ok(code),
};
parser.set_language(&language.into()).map_err(|e| e.to_string())?;
let tree = parser.parse(&code, None).ok_or("Failed to parse code")?;
let mut result_skeleton = String::new();
fn extract_skeleton(node: Node, code: &str, out: &mut String, depth: usize) {
let kind = node.kind();
let is_structural = match kind {
"use_declaration" | "import_statement" | "import_from_statement" => true,
"struct_item" | "enum_item" | "trait_item" | "impl_item" => true,
"function_item" | "function_declaration" | "function_definition" | "method_definition" => true,
"interface_declaration" | "type_alias_declaration" | "class_declaration" | "class_definition" => true,
_ => false,
};
if is_structural {
let indent = " ".repeat(depth);
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
let mut signature = String::new();
for line in node_text.lines() {
let trimmed = line.trim();
if trimmed.ends_with('{') || trimmed.ends_with(':') {
signature.push_str(line);
signature.push_str(" ... }");
break;
} else {
signature.push_str(line);
signature.push('\n');
}
}
if signature.is_empty() {
signature = node_text.to_string();
}
out.push_str(&indent);
out.push_str(&signature.trim());
out.push('\n');
} else if node.is_named() {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_skeleton(child, code, out, depth);
}
}
}
extract_skeleton(tree.root_node(), &code, &mut result_skeleton, 0);
if result_skeleton.is_empty() {
Ok(code)
} else {
Ok(result_skeleton)
}
})
.await
.map_err(|e| format!("Task panic: {}", e))??;
Ok(result)
}
}
+78
View File
@@ -0,0 +1,78 @@
use crate::router::McpTool;
use crate::state::MemoryState;
use crate::tools::GetActiveWorktreeContextTool;
use async_trait::async_trait;
use serde_json::{json, Value};
use std::sync::Arc;
use std::env;
pub struct GetActiveWorktreeContextHandler;
#[async_trait]
impl McpTool for GetActiveWorktreeContextHandler {
fn name(&self) -> &'static str {
"get_active_worktree_context"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<GetActiveWorktreeContextTool>(
"get_active_worktree_context",
"Get the active worktree context, including branch name, modified files, and a truncated git diff.",
)
}
async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
let result = tokio::task::spawn_blocking(move || -> Result<Value, String> {
let cwd = env::current_dir().map_err(|e| e.to_string())?;
let repo = git2::Repository::discover(&cwd).map_err(|e| format!("Not in a git repository: {}", e))?;
let mut branch_name = String::new();
if let Ok(head) = repo.head() {
if let Some(name) = head.shorthand() {
branch_name = name.to_string();
}
}
let mut opts = git2::DiffOptions::new();
let mut diff = None;
// Try to diff against HEAD
if let Ok(tree) = repo.head().and_then(|h| h.peel_to_tree()) {
diff = repo.diff_tree_to_workdir_with_index(Some(&tree), Some(&mut opts)).ok();
}
let mut files = Vec::new();
let mut diff_output = String::new();
if let Some(diff) = diff {
let _ = diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let content = std::str::from_utf8(line.content()).unwrap_or("");
diff_output.push_str(content);
true
});
for delta in diff.deltas() {
if let Some(path) = delta.new_file().path() {
files.push(path.to_string_lossy().into_owned());
}
}
}
// Truncate diff output if it's too large to save tokens
if diff_output.len() > 10000 {
diff_output.truncate(10000);
diff_output.push_str("\n... [Diff truncated due to size]");
}
Ok(json!({
"branch": branch_name,
"modified_files": files,
"diff": diff_output
}))
})
.await
.map_err(|e| format!("Task panic: {}", e))??;
Ok(serde_json::to_string_pretty(&result).unwrap())
}
}
+76
View File
@@ -0,0 +1,76 @@
use crate::router::McpTool;
use crate::state::MemoryState;
use crate::tools::{WatchProcessLogsTool, GetRecentLogsTool};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
use std::io::{Read, Seek, SeekFrom};
use std::fs::File;
pub struct WatchProcessLogsHandler;
#[async_trait]
impl McpTool for WatchProcessLogsHandler {
fn name(&self) -> &'static str {
"watch_process_logs"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<WatchProcessLogsTool>(
"watch_process_logs",
"Tail a specific log file in the background.",
)
}
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
let tool_args: WatchProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
if !std::path::Path::new(&tool_args.file_path).exists() {
return Err(format!("File does not exist: {}", tool_args.file_path));
}
Ok(format!("Started watching logs for {}", tool_args.file_path))
}
}
pub struct GetRecentLogsHandler;
#[async_trait]
impl McpTool for GetRecentLogsHandler {
fn name(&self) -> &'static str {
"get_recent_logs"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<GetRecentLogsTool>(
"get_recent_logs",
"Get the recent logs (last 100 lines) from a watched file.",
)
}
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
let tool_args: GetRecentLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
let mut file = File::open(&tool_args.file_path).map_err(|e| format!("Failed to open file: {}", e))?;
let len = file.metadata().map_err(|e| e.to_string())?.len();
let read_size = std::cmp::min(16 * 1024, len);
file.seek(SeekFrom::End(-(read_size as i64))).map_err(|e| e.to_string())?;
let mut buffer = String::new();
file.read_to_string(&mut buffer).map_err(|e| e.to_string())?;
let lines: Vec<&str> = buffer.lines().collect();
let recent_lines = if lines.len() > 100 {
lines[lines.len() - 100..].join("\n")
} else {
buffer
};
Ok(recent_lines)
})
.await
.map_err(|e| format!("Task panic: {}", e))??;
Ok(result)
}
}
+4
View File
@@ -5,3 +5,7 @@ pub mod notes;
pub mod tasks;
pub mod utils;
pub mod workspaces;
pub mod vision;
pub mod git;
pub mod logs;
pub mod ast;
+195
View File
@@ -0,0 +1,195 @@
use crate::router::McpTool;
use crate::state::MemoryState;
use crate::tools::{ReadClipboardTool, WriteClipboardTool};
use async_trait::async_trait;
use image::{imageops::FilterType, ImageBuffer};
use serde_json::{json, Value};
use std::sync::Arc;
use clipboard_win::{formats, get_clipboard, Clipboard, raw, Setter};
use arboard::ImageData;
use std::borrow::Cow;
pub struct WriteClipboardHandler;
#[async_trait]
impl McpTool for WriteClipboardHandler {
fn name(&self) -> &'static str {
"write_clipboard"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<WriteClipboardTool>(
"write_clipboard",
"Write text, HTML, files, and/or images directly to the OS clipboard.",
)
}
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
let tool_args: WriteClipboardTool =
serde_json::from_value(args).map_err(|e| format!("Invalid args: {}", e))?;
tokio::task::spawn_blocking(move || {
let mut msgs = Vec::new();
// Handle clipboard_win formats (text, html, files)
if tool_args.text.is_some() || tool_args.html.is_some() || tool_args.files.is_some() {
if let Ok(_clip) = Clipboard::new_attempts(3) {
if let Some(text) = &tool_args.text {
if clipboard_win::set_clipboard_string(text).is_ok() {
msgs.push("Wrote text");
}
}
if let Some(html) = &tool_args.html {
if formats::Html::new().unwrap().write_clipboard(html).is_ok() {
msgs.push("Wrote HTML");
}
}
if let Some(files) = &tool_args.files {
if raw::set_file_list(files).is_ok() {
msgs.push("Wrote FileList");
}
}
}
}
// Handle arboard for image
if let Some(image_path) = &tool_args.image_path {
match image::open(image_path) {
Ok(img) => {
let rgba = img.into_rgba8();
let (w, h) = rgba.dimensions();
let img_data = ImageData {
width: w as usize,
height: h as usize,
bytes: Cow::Owned(rgba.into_raw()),
};
if let Ok(mut clipboard) = arboard::Clipboard::new() {
if clipboard.set_image(img_data).is_ok() {
msgs.push("Wrote image");
} else {
return Err("Failed to write image to clipboard".to_string());
}
}
},
Err(e) => return Err(format!("Failed to open image {}: {}", image_path, e)),
}
}
if msgs.is_empty() {
Ok("No valid clipboard data provided or failed to write.".to_string())
} else {
Ok(format!("Successfully populated clipboard: {}", msgs.join(", ")))
}
})
.await
.map_err(|e| format!("Task panic: {}", e))?
}
}
pub struct ReadClipboardHandler;
#[async_trait]
impl McpTool for ReadClipboardHandler {
fn name(&self) -> &'static str {
"read_clipboard"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ReadClipboardTool>(
"read_clipboard",
"Read the current OS clipboard. Extracts text, HTML, file paths, and/or an image if available.",
)
}
async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
let result = tokio::task::spawn_blocking(move || -> Result<Value, String> {
let mut out = serde_json::Map::new();
if let Ok(_clip) = Clipboard::new_attempts(3) {
if let Ok(text) = get_clipboard::<String, _>(formats::Unicode) {
if !text.trim().is_empty() {
out.insert("text".into(), json!(text));
}
}
if let Ok(html) = get_clipboard::<String, _>(formats::Html::new().unwrap()) {
if !html.trim().is_empty() {
out.insert("html".into(), json!(html));
}
}
if let Ok(files) = get_clipboard::<Vec<String>, _>(formats::FileList) {
if !files.is_empty() {
out.insert("files".into(), json!(files));
}
}
}
if let Ok(mut clipboard) = arboard::Clipboard::new() {
if let Ok(image_data) = clipboard.get_image() {
if let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
image_data.width as u32,
image_data.height as u32,
image_data.bytes.into_owned(),
) {
let mut dynamic_img = image::DynamicImage::ImageRgba8(img);
let max_dim = 1024;
if dynamic_img.width() > max_dim || dynamic_img.height() > max_dim {
dynamic_img = dynamic_img.resize(max_dim, max_dim, FilterType::Lanczos3);
}
let rgb_img = dynamic_img.into_rgb8();
let cache_dir = dirs::home_dir()
.unwrap_or_default()
.join(".gemini/mcp_memory/clipboard");
let _ = std::fs::create_dir_all(&cache_dir);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let file_path = cache_dir.join(format!("clipboard_{}.jpg", timestamp));
if rgb_img.save_with_format(&file_path, image::ImageFormat::Jpeg).is_ok() {
out.insert("image_path".into(), json!(file_path.to_string_lossy().to_string()));
}
}
}
}
Ok(Value::Object(out))
})
.await
.map_err(|e| format!("Task panic: {}", e))??;
Ok(serde_json::to_string_pretty(&result).unwrap())
}
}
use crate::tools::ToggleClipboardWatchModeTool;
pub struct ToggleClipboardWatchModeHandler;
#[async_trait]
impl McpTool for ToggleClipboardWatchModeHandler {
fn name(&self) -> &'static str {
"toggle_clipboard_watch_mode"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ToggleClipboardWatchModeTool>(
"toggle_clipboard_watch_mode",
"Enable or disable clipboard watch mode (auto-ingesting into StickyNotes).",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
let tool_args: ToggleClipboardWatchModeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut watch_mode = state.clipboard_watch_mode.write().await;
*watch_mode = tool_args.enable;
if tool_args.enable {
Ok("Clipboard watch mode enabled. Changes will be ingested as StickyNotes.".to_string())
} else {
Ok("Clipboard watch mode disabled.".to_string())
}
}
}
+25 -9
View File
@@ -15,6 +15,7 @@ pub mod embedding;
mod state;
mod store;
mod tools;
mod clipboard_watcher;
use crate::api::rest::GateSetReq;
use crate::router::MemoryHandler;
@@ -88,6 +89,7 @@ pub struct AppState {
handler: Arc<MemoryHandler>,
clients: RwLock<HashMap<String, mpsc::Sender<String>>>,
next_id: AtomicUsize,
pub shutdown_tx: std::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
}
async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
@@ -124,13 +126,20 @@ async fn index_committer_worker(state: Arc<MemoryState>) {
}
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
state.rebuild_index().await;
let state_for_index = Arc::clone(&state);
tokio::spawn(async move {
state_for_index.rebuild_index().await;
tracing::info!("Index rebuild complete.");
});
tokio::spawn(index_committer_worker(Arc::clone(&state)));
tokio::spawn(ttl_sweeper_worker(Arc::clone(&state)));
crate::clipboard_watcher::spawn_watcher(Arc::clone(&state));
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(Arc::clone(&state))),
clients: RwLock::new(HashMap::new()),
next_id: AtomicUsize::new(1),
shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
});
let app_state_clone = Arc::clone(&app_state);
@@ -156,10 +165,11 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
}
});
// UDP Telemetry Listener on port 3001
// UDP Telemetry Listener
let udp_state = Arc::clone(&app_state);
tokio::spawn(async move {
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3001").await {
let port1 = std::env::var("MCP_UDP_PORT1").unwrap_or_else(|_| "3001".to_string());
if let Ok(socket) = tokio::net::UdpSocket::bind(format!("127.0.0.1:{}", port1)).await {
let mut buf = [0; 4096];
loop {
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
@@ -193,10 +203,11 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
}
});
// UDP Neovim Telemetry Listener on port 3002
// UDP Neovim Telemetry Listener
let nvim_udp_state = Arc::clone(&app_state);
tokio::spawn(async move {
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3002").await {
let port2 = std::env::var("MCP_UDP_PORT2").unwrap_or_else(|_| "3002".to_string());
if let Ok(socket) = tokio::net::UdpSocket::bind(format!("127.0.0.1:{}", port2)).await {
let mut buf = [0; 4096];
loop {
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
@@ -265,9 +276,9 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
let app = api::setup::create_router(app_state);
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/ws");
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr)
let port_str = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
tracing::info!("MCP Memory Server running on http://127.0.0.1:{}/ws", port_str);
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", port_str)
.parse()
.expect("Invalid bind address");
@@ -282,7 +293,12 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
return Ok(());
}
};
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
if let Err(e) = axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
})
.await
{
let log_path = dirs::home_dir()
.unwrap_or_default()
.join(".gemini/mcp_memory/daemon_error.log");
+7
View File
@@ -383,6 +383,13 @@ impl MemoryHandler {
register!(workspaces::AddPrChecklistItemHandler);
register!(workspaces::GetPrChecklistHandler);
register!(workspaces::ClearPrChecklistHandler);
register!(vision::ReadClipboardHandler);
register!(vision::WriteClipboardHandler);
register!(git::GetActiveWorktreeContextHandler);
register!(logs::WatchProcessLogsHandler);
register!(logs::GetRecentLogsHandler);
register!(ast::ReadFileSkeletonHandler);
register!(vision::ToggleClipboardWatchModeHandler);
Self {
state,
+2
View File
@@ -14,6 +14,7 @@ pub struct GenericEvent {
pub struct MemoryState {
pub base_dir: PathBuf,
pub clipboard_watch_mode: tokio::sync::RwLock<bool>,
pub graph: Store<KnowledgeGraph>,
pub search_index: RwLock<MemoryIndex>,
pub ledger: Store<Vec<CodeChange>>,
@@ -48,6 +49,7 @@ impl MemoryState {
let db = crate::db::init_redb(&base);
Self {
clipboard_watch_mode: tokio::sync::RwLock::new(false),
graph: Store::new("knowledge_graph_master", db.clone()),
base_dir: base.clone(),
search_index: RwLock::new(match crate::search::MemoryIndex::new(&base) {
+19
View File
@@ -0,0 +1,19 @@
use arboard::{Clipboard, ImageData};
use image::GenericImageView;
use std::borrow::Cow;
fn main() {
let img = image::open("C:/Users/reazul.ashraf/workspace/rust/mcp-memory/test.jpg").unwrap_or_else(|_| image::DynamicImage::new_rgb8(10, 10));
let rgba = img.into_rgba8();
let (w, h) = rgba.dimensions();
let img_data = ImageData {
width: w as usize,
height: h as usize,
bytes: Cow::Owned(rgba.into_raw()),
};
let mut clipboard = Clipboard::new().unwrap();
clipboard.set_image(img_data).unwrap();
println!("Successfully wrote image");
}
+42
View File
@@ -1,6 +1,23 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Extracts the current contents of the OS clipboard (text, HTML, files, and image).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadClipboardTool {}
/// Writes contents to the OS clipboard. You can provide any combination of text, HTML, file paths, or an image path.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct WriteClipboardTool {
/// Optional plain text to write to the clipboard.
pub text: Option<String>,
/// Optional HTML to write to the clipboard (useful for rich text pasting).
pub html: Option<String>,
/// Optional absolute file paths to write to the clipboard as a Windows File Drop.
pub files: Option<Vec<String>>,
/// Optional absolute path to a local image to write to the clipboard.
pub image_path: Option<String>,
}
/// Create new entities in the knowledge graph.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateEntitiesTool {
@@ -637,3 +654,28 @@ mod tests {
);
}
}
/// Get the active worktree context, including branch name, modified files, and a truncated git diff.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetActiveWorktreeContextTool {}
/// Tail a specific log file in the background so it can be queried later.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct WatchProcessLogsTool {
pub file_path: String,
}
/// Get the recent logs from a watched file.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetRecentLogsTool {
pub file_path: String,
}
/// Enable or disable clipboard watch mode (auto-ingesting into StickyNotes).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ToggleClipboardWatchModeTool {
pub enable: bool,
}
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadFileSkeletonTool {
pub file_path: String,
}