refactor: deduplicate arg parsing in handlers and remove redundant python scripts
Created a parse_tool! macro that eliminated ~450 lines of repetitive error-handling boilerplate across 54 MCP tool definitions in server/src/handlers.rs. Purged 30+ stale, redundant python patching scripts and legacy mcp_client.py test runners that were polluting the workspace root and server/src directories.
This commit is contained in:
1 parent
ee4157d07b
commit
0e29b12ac8
33 files changed
+71
-1981
No files matched your search
@@ -1,47 +0,0 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
echo Building mcp-memory (server and stub) for Windows...
|
||||
rustup run stable cargo build --release --workspace --exclude mcp-memory-linux-nvim
|
||||
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 --workspace --exclude mcp-memory-win-nvim --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\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
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import sys
|
||||
|
||||
handlers_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\handlers.rs"
|
||||
with open(handlers_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace(
|
||||
'return Some(crate::mcp::success(id.clone(), serde_json::json!({"isError": true, "content": [{"type": "text", "text": format!("Invalid args: {}", e)}] })));',
|
||||
'return Some(crate::mcp::success(id.clone(), serde_json::json!({"isError": true, "content": [{"type": "text", "text": format!("Invalid args: {}", e)}] })))'
|
||||
)
|
||||
content = content.replace('let graph = self.state.graph.read();', 'let graph = self.state.get_full_graph();')
|
||||
|
||||
with open(handlers_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Fixed handlers.rs again")
|
||||
|
||||
main_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
with open(main_path, "r", encoding="utf-8") as f:
|
||||
main_content = f.read()
|
||||
|
||||
main_content = main_content.replace('state_git.code_changes.modify(|changes| {', 'state_git.ledger.modify(|changes| {')
|
||||
|
||||
with open(main_path, "w", encoding="utf-8") as f:
|
||||
f.write(main_content)
|
||||
print("Fixed main.rs again")
|
||||
@@ -1,186 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/main.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Remove ledger and sticky GC from reconcile_worker
|
||||
reconcile_worker_old = """
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
state.ledger.modify(|ledger| {
|
||||
let seven_days = now.saturating_sub(7 * 24 * 60 * 60);
|
||||
ledger.retain(|c| c.timestamp >= seven_days);
|
||||
if ledger.len() > 1000 {
|
||||
let excess = ledger.len() - 1000;
|
||||
ledger.drain(0..excess);
|
||||
}
|
||||
});
|
||||
state.sticky.modify(|notes| {
|
||||
notes.retain(|note| note.timestamp >= now.saturating_sub(24 * 60 * 60));
|
||||
});"""
|
||||
|
||||
content = content.replace(reconcile_worker_old, "")
|
||||
|
||||
# 2. Extract Task GC from main and create garbage_collector_worker
|
||||
gc_loop_old = """ // Background Garbage Collection for old tasks
|
||||
let state_gc = Arc::clone(&state);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
// Run every 24 hours
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(24 * 3600)).await;
|
||||
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let fourteen_days = 14 * 24 * 3600;
|
||||
let cutoff = now.saturating_sub(fourteen_days);
|
||||
|
||||
state_gc.tasks.modify(|tasks| {
|
||||
let initial_len = tasks.len();
|
||||
tasks.retain(|task| {
|
||||
if task.status.to_lowercase() == "completed" && task.created_at < cutoff {
|
||||
false // remove
|
||||
} else {
|
||||
true // keep
|
||||
}
|
||||
});
|
||||
if tasks.len() < initial_len {
|
||||
eprintln!("GC: Removed {} old completed tasks", initial_len - tasks.len());
|
||||
}
|
||||
});
|
||||
}
|
||||
});"""
|
||||
|
||||
gc_worker_new = """async fn garbage_collector_worker(state: Arc<MemoryState>) {
|
||||
loop {
|
||||
// Run every 6 hours
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(6 * 3600)).await;
|
||||
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
|
||||
// 1. Task GC (14 days)
|
||||
let fourteen_days = 14 * 24 * 3600;
|
||||
let task_cutoff = now.saturating_sub(fourteen_days);
|
||||
state.tasks.modify(|tasks| {
|
||||
let initial_len = tasks.len();
|
||||
tasks.retain(|task| !(task.status.to_lowercase() == "completed" && task.created_at < task_cutoff));
|
||||
if tasks.len() < initial_len {
|
||||
eprintln!("GC: Removed {} old completed tasks", initial_len - tasks.len());
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Ledger GC (7 days or max 1000 items)
|
||||
state.ledger.modify(|ledger| {
|
||||
let seven_days = now.saturating_sub(7 * 24 * 3600);
|
||||
ledger.retain(|c| c.timestamp >= seven_days);
|
||||
if ledger.len() > 1000 {
|
||||
let excess = ledger.len() - 1000;
|
||||
ledger.drain(0..excess);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Sticky Notes GC (24 hours)
|
||||
state.sticky.modify(|notes| {
|
||||
notes.retain(|note| note.timestamp >= now.saturating_sub(24 * 3600));
|
||||
});
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
content = content.replace(gc_loop_old, " tokio::spawn(garbage_collector_worker(Arc::clone(&state)));")
|
||||
content = content.replace("async fn reconcile_worker", gc_worker_new + "\nasync fn reconcile_worker")
|
||||
|
||||
# 3. Extract Git Native Sync from main
|
||||
git_loop_old = """ // Git Native Sync Background Task
|
||||
let state_git = Arc::clone(&state);
|
||||
tokio::spawn(async move {
|
||||
let repo_path = std::env::current_dir().unwrap_or_else(|_| ".".into());
|
||||
let mut last_commit_id = String::new();
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
|
||||
|
||||
if let Ok(repo) = git2::Repository::discover(&repo_path) {
|
||||
if let Ok(head) = repo.head() {
|
||||
if let Ok(commit) = head.peel_to_commit() {
|
||||
let current_id = commit.id().to_string();
|
||||
if current_id != last_commit_id && !last_commit_id.is_empty() {
|
||||
let msg = commit.message().unwrap_or("").to_string();
|
||||
let branch = head.shorthand().unwrap_or("unknown").to_string();
|
||||
|
||||
state_git.ledger.modify(|changes| {
|
||||
changes.push(crate::models::CodeChange {
|
||||
git_commit: Some(current_id.clone()),
|
||||
git_branch: Some(branch),
|
||||
description: format!("Auto-synced commit: {}", msg.trim()),
|
||||
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
||||
file_path: "".to_string(),
|
||||
});
|
||||
});
|
||||
eprintln!("Git Sync: Logged new commit {}", current_id);
|
||||
|
||||
state_git.tasks.modify(|tasks| {
|
||||
for task in tasks.iter_mut() {
|
||||
if task.status != "completed" && msg.to_lowercase().contains(&task.title.to_lowercase()) {
|
||||
task.status = "completed".to_string();
|
||||
eprintln!("Git Sync: Auto-completed task '{}'", task.title);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
last_commit_id = current_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});"""
|
||||
|
||||
git_worker_new = """async fn git_sync_worker(state: Arc<MemoryState>) {
|
||||
let repo_path = std::env::current_dir().unwrap_or_else(|_| ".".into());
|
||||
let mut last_commit_id = String::new();
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
|
||||
|
||||
if let Ok(repo) = git2::Repository::discover(&repo_path) {
|
||||
if let Ok(head) = repo.head() {
|
||||
if let Ok(commit) = head.peel_to_commit() {
|
||||
let current_id = commit.id().to_string();
|
||||
if current_id != last_commit_id && !last_commit_id.is_empty() {
|
||||
let msg = commit.message().unwrap_or("").to_string();
|
||||
let branch = head.shorthand().unwrap_or("unknown").to_string();
|
||||
|
||||
state.ledger.modify(|changes| {
|
||||
changes.push(crate::models::CodeChange {
|
||||
git_commit: Some(current_id.clone()),
|
||||
git_branch: Some(branch),
|
||||
description: format!("Auto-synced commit: {}", msg.trim()),
|
||||
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
||||
file_path: "".to_string(),
|
||||
});
|
||||
});
|
||||
eprintln!("Git Sync: Logged new commit {}", current_id);
|
||||
|
||||
state.tasks.modify(|tasks| {
|
||||
for task in tasks.iter_mut() {
|
||||
if task.status != "completed" && msg.to_lowercase().contains(&task.title.to_lowercase()) {
|
||||
task.status = "completed".to_string();
|
||||
eprintln!("Git Sync: Auto-completed task '{}'", task.title);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
last_commit_id = current_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
content = content.replace(git_loop_old, " tokio::spawn(git_sync_worker(Arc::clone(&state)));")
|
||||
content = content.replace("async fn reconcile_worker", git_worker_new + "\nasync fn reconcile_worker")
|
||||
|
||||
with open("server/src/main.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/main.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
cli_gate_block = re.search(r' if let Some\(command\) = cli\.command \{.*?(?= run_server\(state\))', content, re.DOTALL)
|
||||
if cli_gate_block:
|
||||
content = content.replace(cli_gate_block.group(0), "")
|
||||
|
||||
new_cli_logic = """
|
||||
if let Some(command) = cli.command {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let client = reqwest::Client::new();
|
||||
match command {
|
||||
Commands::Gate { subcmd } => match subcmd {
|
||||
GateCommands::Set { action, target, namespace, params, authorize, block, reason } => {
|
||||
let mut param_map = HashMap::new();
|
||||
for p in params {
|
||||
if let Some((k, v)) = p.split_once('=') {
|
||||
param_map.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
}
|
||||
let body = serde_json::json!({
|
||||
"action": action,
|
||||
"target": target,
|
||||
"namespace": namespace,
|
||||
"params": param_map,
|
||||
"authorize": authorize,
|
||||
"block": block,
|
||||
"reason": reason
|
||||
});
|
||||
match client.post("http://127.0.0.1:3000/gate/set").json(&body).send().await {
|
||||
Ok(res) if res.status().is_success() => {
|
||||
println!("Gate state updated via daemon.");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Ok(res) => {
|
||||
eprintln!("Failed to update gate: {}", res.status());
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to daemon: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
GateCommands::Verify { action, target, namespace, params, consume } => {
|
||||
let mut query = vec![
|
||||
("action".to_string(), action),
|
||||
("target".to_string(), target),
|
||||
("consume".to_string(), consume.to_string()),
|
||||
];
|
||||
if let Some(ns) = namespace {
|
||||
query.push(("namespace".to_string(), ns));
|
||||
}
|
||||
// Reqwest will serialize params correctly if we pass the right struct.
|
||||
// Actually, axum's Query extractor for HashMap requires flat keys or standard serialization.
|
||||
for p in params {
|
||||
if let Some((k, v)) = p.split_once('=') {
|
||||
query.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
}
|
||||
match client.get("http://127.0.0.1:3000/gate/verify").query(&query).send().await {
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
let text = res.text().await.unwrap_or_default();
|
||||
if status.is_success() {
|
||||
std::process::exit(0);
|
||||
} else if status == reqwest::StatusCode::FORBIDDEN {
|
||||
eprintln!("❌ {}", text);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
eprintln!("❌ {}", text);
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to daemon: {}", e);
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
"""
|
||||
|
||||
restart_block = re.search(r' if cli\.restart \{.*?return Ok\(\(\);\n \}', content, re.DOTALL)
|
||||
if restart_block:
|
||||
content = content.replace(restart_block.group(0), restart_block.group(0) + "\n" + new_cli_logic)
|
||||
|
||||
with open("server/src/main.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/main.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
git_worker_old = """async fn git_sync_worker(state: Arc<MemoryState>) {
|
||||
let repo_path = std::env::current_dir().unwrap_or_else(|_| ".".into());
|
||||
let mut last_commit_id = String::new();
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
|
||||
|
||||
if let Ok(repo) = git2::Repository::discover(&repo_path) {
|
||||
if let Ok(head) = repo.head() {
|
||||
if let Ok(commit) = head.peel_to_commit() {
|
||||
let current_id = commit.id().to_string();
|
||||
if current_id != last_commit_id && !last_commit_id.is_empty() {
|
||||
let msg = commit.message().unwrap_or("").to_string();
|
||||
let branch = head.shorthand().unwrap_or("unknown").to_string();
|
||||
|
||||
state.ledger.modify(|changes| {
|
||||
changes.push(crate::models::CodeChange {
|
||||
git_commit: Some(current_id.clone()),
|
||||
git_branch: Some(branch),
|
||||
description: format!("Auto-synced commit: {}", msg.trim()),
|
||||
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
||||
file_path: "".to_string(),
|
||||
});
|
||||
});
|
||||
eprintln!("Git Sync: Logged new commit {}", current_id);
|
||||
|
||||
state.tasks.modify(|tasks| {
|
||||
for task in tasks.iter_mut() {
|
||||
if task.status != "completed" && msg.to_lowercase().contains(&task.title.to_lowercase()) {
|
||||
task.status = "completed".to_string();
|
||||
eprintln!("Git Sync: Auto-completed task '{}'", task.title);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
last_commit_id = current_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
git_worker_new = """async fn git_sync_worker(state: Arc<MemoryState>) {
|
||||
let repo_path = std::env::current_dir().unwrap_or_else(|_| ".".into());
|
||||
let mut last_commit_id = String::new();
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
|
||||
|
||||
let repo_path_clone = repo_path.clone();
|
||||
let commit_data = tokio::task::spawn_blocking(move || {
|
||||
if let Ok(repo) = git2::Repository::discover(&repo_path_clone) {
|
||||
if let Ok(head) = repo.head() {
|
||||
if let Ok(commit) = head.peel_to_commit() {
|
||||
let current_id = commit.id().to_string();
|
||||
let msg = commit.message().unwrap_or("").to_string();
|
||||
let branch = head.shorthand().unwrap_or("unknown").to_string();
|
||||
return Some((current_id, msg, branch));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if let Some((current_id, msg, branch)) = commit_data {
|
||||
if current_id != last_commit_id && !last_commit_id.is_empty() {
|
||||
state.ledger.modify(|changes| {
|
||||
changes.push(crate::models::CodeChange {
|
||||
git_commit: Some(current_id.clone()),
|
||||
git_branch: Some(branch),
|
||||
description: format!("Auto-synced commit: {}", msg.trim()),
|
||||
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
||||
file_path: "".to_string(),
|
||||
});
|
||||
});
|
||||
eprintln!("Git Sync: Logged new commit {}", current_id);
|
||||
|
||||
state.tasks.modify(|tasks| {
|
||||
for task in tasks.iter_mut() {
|
||||
if task.status != "completed" && msg.to_lowercase().contains(&task.title.to_lowercase()) {
|
||||
task.status = "completed".to_string();
|
||||
eprintln!("Git Sync: Auto-completed task '{}'", task.title);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
last_commit_id = current_id;
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
content = content.replace(git_worker_old, git_worker_new)
|
||||
|
||||
with open("server/src/main.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import sys
|
||||
|
||||
cargo_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\Cargo.toml"
|
||||
with open(cargo_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace git2 to use vendored openssl to avoid cross-compilation linking issues
|
||||
content = content.replace('git2 = "0.19.0"', 'git2 = { version = "0.19.0", features = ["vendored-openssl"] }')
|
||||
|
||||
with open(cargo_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Added vendored-openssl feature to git2")
|
||||
@@ -1,15 +0,0 @@
|
||||
import sys
|
||||
|
||||
handlers_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\handlers.rs"
|
||||
with open(handlers_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace(
|
||||
'return Some(crate::mcp::success(req_id, true, &format!("Invalid args: {}", e))),',
|
||||
'return Some(crate::mcp::success(id.clone(), serde_json::json!({"isError": true, "content": [{"type": "text", "text": format!("Invalid args: {}", e)}] })));'
|
||||
)
|
||||
content = content.replace('let graph = state.graph.read();', 'let graph = self.state.graph.read();')
|
||||
|
||||
with open(handlers_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Fixed handlers.rs")
|
||||
@@ -1,76 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/state.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
rebuild_index_old = """ pub fn rebuild_index(&self) {
|
||||
if let Ok(new_idx) = MemoryIndex::new(&self.base_dir) {
|
||||
let session = self.session_graph.read().unwrap();
|
||||
let mut full = {
|
||||
let cache = self.master_cache.read().unwrap();
|
||||
cache.0.clone()
|
||||
};
|
||||
Self::merge_graphs(&mut full, &session);
|
||||
for e in full.entities.values() {
|
||||
let _ = new_idx.index_entity(e);
|
||||
}
|
||||
for t in self.tasks.read() {
|
||||
let _ = new_idx.index_task(&t);
|
||||
}
|
||||
for s in self.snippets.read() {
|
||||
let _ = new_idx.index_snippet(&s);
|
||||
}
|
||||
for a in self.adrs.read() {
|
||||
let _ = new_idx.index_adr(&a);
|
||||
}
|
||||
if let Ok(mut w) = self.search_index.write() {
|
||||
*w = new_idx;
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
rebuild_index_new = """ pub fn rebuild_index(&self) {
|
||||
if let Ok(new_idx) = MemoryIndex::new(&self.base_dir) {
|
||||
let session = self.session_graph.read().unwrap();
|
||||
let cache = self.master_cache.read().unwrap();
|
||||
|
||||
// Index entities that are only in master, or merge if they are in both
|
||||
for (name, e) in &cache.0.entities {
|
||||
if let Some(session_e) = session.entities.get(name) {
|
||||
let mut merged_e = e.clone();
|
||||
for obs in &session_e.observations {
|
||||
if !merged_e.observations.contains(obs) {
|
||||
merged_e.observations.push(obs.clone());
|
||||
}
|
||||
}
|
||||
let _ = new_idx.index_entity(&merged_e);
|
||||
} else {
|
||||
let _ = new_idx.index_entity(e);
|
||||
}
|
||||
}
|
||||
// Index entities that are only in session
|
||||
for (name, session_e) in &session.entities {
|
||||
if !cache.0.entities.contains_key(name) {
|
||||
let _ = new_idx.index_entity(session_e);
|
||||
}
|
||||
}
|
||||
|
||||
for t in self.tasks.read() {
|
||||
let _ = new_idx.index_task(&t);
|
||||
}
|
||||
for s in self.snippets.read() {
|
||||
let _ = new_idx.index_snippet(&s);
|
||||
}
|
||||
for a in self.adrs.read() {
|
||||
let _ = new_idx.index_adr(&a);
|
||||
}
|
||||
if let Ok(mut w) = self.search_index.write() {
|
||||
*w = new_idx;
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
content = content.replace(rebuild_index_old, rebuild_index_new)
|
||||
|
||||
with open("server/src/state.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
@@ -1,8 +0,0 @@
|
||||
import re
|
||||
with open(r'C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs', 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
text = re.sub(r'#\[cfg\(not\(target_os = "windows"\)\)\].*?return Ok\(\(\);\n \}', '', text, flags=re.DOTALL)
|
||||
|
||||
with open(r'C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs', 'w', encoding='utf-8') as f:
|
||||
f.write(text)
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import sys
|
||||
|
||||
main_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
with open(main_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Remove WS code
|
||||
ws_start = content.find('async fn ws_handler')
|
||||
if ws_start != -1:
|
||||
content = content[:ws_start]
|
||||
|
||||
# Remove /ws route
|
||||
route_idx = content.find('.route("/ws", get(ws_handler))')
|
||||
if route_idx != -1:
|
||||
content = content.replace('.route("/ws", get(ws_handler))\n ', '')
|
||||
|
||||
# Remove WS import
|
||||
import_idx = content.find('use axum::extract::ws::{WebSocketUpgrade, WebSocket, Message};\n')
|
||||
if import_idx != -1:
|
||||
content = content.replace('use axum::extract::ws::{WebSocketUpgrade, WebSocket, Message};\n', '')
|
||||
|
||||
# Fix GC task state
|
||||
gc_start = content.find(' // Background Garbage Collection for old tasks')
|
||||
if gc_start != -1:
|
||||
content = content.replace('let state_gc = state_clone.clone();', 'let state_gc = Arc::clone(&state);')
|
||||
|
||||
# Fix Git task state and CodeChange fields
|
||||
git_start = content.find(' // Git Native Sync Background Task')
|
||||
if git_start != -1:
|
||||
content = content.replace('let state_git = state_clone.clone();', 'let state_git = Arc::clone(&state);')
|
||||
content = content.replace('commit_hash: current_id.clone(),', 'git_commit: Some(current_id.clone()),')
|
||||
content = content.replace('branch: branch,', 'git_branch: Some(branch),')
|
||||
content = content.replace('files_changed: vec![],', 'file_path: "".to_string(),')
|
||||
|
||||
with open(main_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Cleaned up WS and fixed main.rs")
|
||||
@@ -1,52 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/state.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
unique_items_block = re.search(r' pub fn unique_items<.*?>\(.*?\) -> Vec<T> \{\n(?:.*?\n){1,15}? \}\n', content, re.DOTALL)
|
||||
if unique_items_block:
|
||||
content = content.replace(unique_items_block.group(0), "")
|
||||
|
||||
merge_graphs_old = """ pub fn merge_graphs(dest: &mut KnowledgeGraph, src: &KnowledgeGraph) {
|
||||
for (name, src_ent) in &src.entities {
|
||||
let dest_ent = dest
|
||||
.entities
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| src_ent.clone());
|
||||
if dest_ent.name == src_ent.name {
|
||||
dest_ent.observations.extend(src_ent.observations.clone());
|
||||
dest_ent.observations = Self::unique_items(dest_ent.observations.clone());
|
||||
}
|
||||
}
|
||||
dest.relations.extend(src.relations.clone());
|
||||
dest.relations = Self::unique_items(dest.relations.clone());
|
||||
}"""
|
||||
|
||||
merge_graphs_new = """ pub fn merge_graphs(dest: &mut KnowledgeGraph, src: &KnowledgeGraph) {
|
||||
for (name, src_ent) in &src.entities {
|
||||
let dest_ent = dest
|
||||
.entities
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| crate::models::Entity {
|
||||
name: src_ent.name.clone(),
|
||||
entity_type: src_ent.entity_type.clone(),
|
||||
observations: Vec::new(),
|
||||
});
|
||||
|
||||
for obs in &src_ent.observations {
|
||||
if !dest_ent.observations.contains(obs) {
|
||||
dest_ent.observations.push(obs.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for rel in &src.relations {
|
||||
if !dest.relations.contains(rel) {
|
||||
dest.relations.push(rel.clone());
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
content = content.replace(merge_graphs_old, merge_graphs_new)
|
||||
|
||||
with open("server/src/state.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import sys
|
||||
|
||||
main_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
with open(main_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace(
|
||||
'handler: Arc::new(MemoryHandler { state }),',
|
||||
'handler: Arc::new(MemoryHandler { state: Arc::clone(&state) }),'
|
||||
)
|
||||
|
||||
with open(main_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Fixed main.rs move error")
|
||||
@@ -1,56 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/main.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
reconcile_worker_old = """async fn reconcile_worker(state: Arc<MemoryState>) {
|
||||
loop {
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
let pattern = format!("{}/delta_*.json", state.base_dir.display());
|
||||
let has_local = {
|
||||
let session = state.session_graph.read().unwrap();
|
||||
!session.entities.is_empty() || !session.relations.is_empty()
|
||||
};
|
||||
let has_files = glob::glob(&pattern).map(|p| p.count() > 0).unwrap_or(false);
|
||||
if has_local || has_files {
|
||||
state.apply_sync_write(|_master| {}).await;
|
||||
let state_clone = state.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
state_clone.rebuild_index();
|
||||
}).await;
|
||||
}
|
||||
|
||||
}
|
||||
}"""
|
||||
|
||||
reconcile_worker_new = """async fn reconcile_worker(state: Arc<MemoryState>) {
|
||||
loop {
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
|
||||
let has_local = {
|
||||
let session = state.session_graph.read().unwrap();
|
||||
!session.entities.is_empty() || !session.relations.is_empty()
|
||||
};
|
||||
|
||||
let base_dir = state.base_dir.clone();
|
||||
let has_files = tokio::task::spawn_blocking(move || {
|
||||
let pattern = format!("{}/delta_*.json", base_dir.display());
|
||||
glob::glob(&pattern).map(|p| p.count() > 0).unwrap_or(false)
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_local || has_files {
|
||||
state.apply_sync_write(|_master| {}).await;
|
||||
let state_clone = state.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
state_clone.rebuild_index();
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
content = content.replace(reconcile_worker_old, reconcile_worker_new)
|
||||
|
||||
with open("server/src/main.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
@@ -1,49 +0,0 @@
|
||||
import re
|
||||
|
||||
main_rs_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
|
||||
with open(main_rs_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace the broken route
|
||||
old_route = r""" .route("/api/tasks/:id/complete", post({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||
if let Ok(mut tasks) = state_clone.tasks.write() {
|
||||
let mut found = false;
|
||||
for t in tasks.iter_mut() {
|
||||
if t.id == id {
|
||||
t.status = "completed".to_string();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if found {
|
||||
let _ = state_clone.flush_store();
|
||||
return axum::Json(serde_json::json!({"status": "success"}));
|
||||
}
|
||||
}
|
||||
axum::Json(serde_json::json!({"status": "not_found"}))
|
||||
}
|
||||
}))"""
|
||||
|
||||
new_route = """ .route("/api/tasks/:id/complete", post({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||
state_clone.tasks.modify(|tasks| {
|
||||
for t in tasks.iter_mut() {
|
||||
if t.id == id {
|
||||
t.status = "completed".to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
axum::Json(serde_json::json!({"status": "success"}))
|
||||
}
|
||||
}))"""
|
||||
|
||||
content = content.replace(old_route, new_route)
|
||||
|
||||
with open(main_rs_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Replaced route.")
|
||||
@@ -1,12 +0,0 @@
|
||||
import re
|
||||
|
||||
main_rs_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
|
||||
with open(main_rs_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace('.route("/api/tasks/:id/complete"', '.route("/api/tasks/{id}/complete"')
|
||||
|
||||
with open(main_rs_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Replaced route syntax.")
|
||||
@@ -1,19 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/search.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Add pub fn commit
|
||||
commit_code = """
|
||||
pub fn commit(&self) -> tantivy::Result<()> {
|
||||
let mut writer = self.writer.lock().unwrap();
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
"""
|
||||
|
||||
content = content.replace(" pub fn search(", commit_code + "\n pub fn search(")
|
||||
content = content.replace("let mut writer = self.writer.lock().unwrap();", "let writer = self.writer.lock().unwrap();")
|
||||
|
||||
with open("server/src/search.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
@@ -1,21 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/state.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Add git_branch and namespace to Entity initialization
|
||||
content = content.replace("observations: Vec::new(),", "observations: Vec::new(),\n namespace: src_ent.namespace.clone(),\n git_branch: src_ent.git_branch.clone(),")
|
||||
|
||||
# Add unique_items back
|
||||
unique_items_code = """
|
||||
pub fn unique_items<T: Eq + std::hash::Hash + Clone>(input: Vec<T>) -> Vec<T> {
|
||||
let mut keys = std::collections::HashSet::new();
|
||||
input.into_iter().filter(|entry| keys.insert(entry.clone())).collect()
|
||||
}
|
||||
"""
|
||||
|
||||
content = content.replace("impl MemoryState {\n", "impl MemoryState {\n" + unique_items_code)
|
||||
|
||||
with open("server/src/state.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/search.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Remove writer.commit() from index_*
|
||||
content = content.replace(" writer.commit()?;\n", "")
|
||||
|
||||
# Add pub fn commit
|
||||
commit_code = """
|
||||
pub fn commit(&self) -> tantivy::Result<()> {
|
||||
let mut writer = self.writer.lock().unwrap();
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
"""
|
||||
|
||||
content = content.replace(" pub fn search(&self, query_str: &str, limit: usize) -> tantivy::Result<Vec<SearchResult>> {", commit_code + "\n pub fn search(&self, query_str: &str, limit: usize) -> tantivy::Result<Vec<SearchResult>> {")
|
||||
|
||||
with open("server/src/search.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
with open("server/src/state.rs", "r", encoding="utf-8") as f:
|
||||
state_content = f.read()
|
||||
|
||||
# Call new_idx.commit() in rebuild_index before locking the main rwlock
|
||||
state_content = state_content.replace(""" if let Ok(mut w) = self.search_index.write() {
|
||||
*w = new_idx;
|
||||
}""", """ let _ = new_idx.commit();
|
||||
if let Ok(mut w) = self.search_index.write() {
|
||||
*w = new_idx;
|
||||
}""")
|
||||
|
||||
with open("server/src/state.rs", "w", encoding="utf-8") as f:
|
||||
f.write(state_content)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import sys
|
||||
|
||||
tools_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\tools.rs"
|
||||
with open(tools_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace("use schemars::JsonSchema;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]\npub struct QueryGraphPathTool", "#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]\npub struct QueryGraphPathTool")
|
||||
|
||||
with open(tools_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Fixed tools.rs")
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/state.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
recover_wal_code = """
|
||||
pub fn recover_wal(&self) {
|
||||
let wal_path = self.base_dir.join("wal.jsonl");
|
||||
if let Ok(content) = std::fs::read_to_string(&wal_path) {
|
||||
let mut session = self.session_graph.write().unwrap();
|
||||
for line in content.lines() {
|
||||
if let Ok(d) = serde_json::from_str::<KnowledgeGraph>(line) {
|
||||
Self::merge_graphs(&mut session, &d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
content = content.replace(" pub fn merge_graphs(dest: &mut KnowledgeGraph, src: &KnowledgeGraph) {", recover_wal_code + " pub fn merge_graphs(dest: &mut KnowledgeGraph, src: &KnowledgeGraph) {")
|
||||
|
||||
get_full_graph_old = """ pub fn get_full_graph(&self) -> KnowledgeGraph {
|
||||
let mut master = self.read_master_cached();
|
||||
let wal_path = self.base_dir.join("wal.jsonl");
|
||||
if let Ok(content) = std::fs::read_to_string(&wal_path) {
|
||||
for line in content.lines() {
|
||||
if let Ok(d) = serde_json::from_str::<KnowledgeGraph>(line) {
|
||||
Self::merge_graphs(&mut master, &d);
|
||||
}
|
||||
}
|
||||
}
|
||||
let session_graph = self.session_graph.read().unwrap();
|
||||
Self::merge_graphs(&mut master, &session_graph);
|
||||
master
|
||||
}"""
|
||||
|
||||
get_full_graph_new = """ pub fn get_full_graph(&self) -> KnowledgeGraph {
|
||||
let mut master = self.read_master_cached();
|
||||
let session_graph = self.session_graph.read().unwrap();
|
||||
Self::merge_graphs(&mut master, &session_graph);
|
||||
master
|
||||
}"""
|
||||
|
||||
content = content.replace(get_full_graph_old, get_full_graph_new)
|
||||
|
||||
with open("server/src/state.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
with open("server/src/main.rs", "r", encoding="utf-8") as f:
|
||||
main_content = f.read()
|
||||
|
||||
main_content = main_content.replace(" state.rebuild_index();\n\n run_server(state)", " state.recover_wal();\n state.rebuild_index();\n\n run_server(state)")
|
||||
|
||||
with open("server/src/main.rs", "w", encoding="utf-8") as f:
|
||||
f.write(main_content)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("server/src/search.rs", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace("let writer = self.writer.lock().unwrap();\n writer.commit()?;", "let mut writer = self.writer.lock().unwrap();\n writer.commit()?;")
|
||||
|
||||
with open("server/src/search.rs", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
@@ -1,45 +0,0 @@
|
||||
import sys
|
||||
|
||||
main_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
with open(main_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
idx = content.find('eprintln!("MCP Memory Server running on http://127.0.0.1:3000/sse");')
|
||||
if idx == -1:
|
||||
print("Could not find start point")
|
||||
sys.exit(1)
|
||||
|
||||
gc_code = """
|
||||
// Background Garbage Collection for old tasks
|
||||
let state_gc = state_clone.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
// Run every 24 hours
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(24 * 3600)).await;
|
||||
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let fourteen_days = 14 * 24 * 3600;
|
||||
let cutoff = now.saturating_sub(fourteen_days);
|
||||
|
||||
state_gc.tasks.modify(|tasks| {
|
||||
let initial_len = tasks.len();
|
||||
tasks.retain(|task| {
|
||||
if task.status.to_lowercase() == "completed" && task.created_at < cutoff {
|
||||
false // remove
|
||||
} else {
|
||||
true // keep
|
||||
}
|
||||
});
|
||||
if tasks.len() < initial_len {
|
||||
eprintln!("GC: Removed {} old completed tasks", initial_len - tasks.len());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
"""
|
||||
|
||||
content = content[:idx] + gc_code + content[idx:]
|
||||
with open(main_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Injected GC task!")
|
||||
@@ -1,62 +0,0 @@
|
||||
import sys
|
||||
|
||||
main_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
with open(main_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
idx = content.find('eprintln!("MCP Memory Server running on http://127.0.0.1:3000/sse");')
|
||||
if idx == -1:
|
||||
print("Could not find start point")
|
||||
sys.exit(1)
|
||||
|
||||
git_code = """
|
||||
// Git Native Sync Background Task
|
||||
let state_git = state_clone.clone();
|
||||
tokio::spawn(async move {
|
||||
let repo_path = std::env::current_dir().unwrap_or_else(|_| ".".into());
|
||||
let mut last_commit_id = String::new();
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
|
||||
|
||||
if let Ok(repo) = git2::Repository::discover(&repo_path) {
|
||||
if let Ok(head) = repo.head() {
|
||||
if let Ok(commit) = head.peel_to_commit() {
|
||||
let current_id = commit.id().to_string();
|
||||
if current_id != last_commit_id && !last_commit_id.is_empty() {
|
||||
let msg = commit.message().unwrap_or("").to_string();
|
||||
let branch = head.shorthand().unwrap_or("unknown").to_string();
|
||||
|
||||
state_git.code_changes.modify(|changes| {
|
||||
changes.push(crate::models::CodeChange {
|
||||
commit_hash: current_id.clone(),
|
||||
branch: branch,
|
||||
description: format!("Auto-synced commit: {}", msg.trim()),
|
||||
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
||||
files_changed: vec![],
|
||||
});
|
||||
});
|
||||
eprintln!("Git Sync: Logged new commit {}", current_id);
|
||||
|
||||
state_git.tasks.modify(|tasks| {
|
||||
for task in tasks.iter_mut() {
|
||||
if task.status != "completed" && msg.to_lowercase().contains(&task.title.to_lowercase()) {
|
||||
task.status = "completed".to_string();
|
||||
eprintln!("Git Sync: Auto-completed task '{}'", task.title);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
last_commit_id = current_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
"""
|
||||
|
||||
content = content[:idx] + git_code + content[idx:]
|
||||
with open(main_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Injected Git Sync task!")
|
||||
@@ -1,92 +0,0 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
handlers_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\handlers.rs"
|
||||
|
||||
with open(handlers_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Add to list_tools
|
||||
list_tools_idx = content.find('crate::mcp::tool_def::<CreateEntitiesTool>')
|
||||
if list_tools_idx == -1:
|
||||
print("Could not find list_tools")
|
||||
sys.exit(1)
|
||||
|
||||
tool_def = ' crate::mcp::tool_def::<crate::tools::QueryGraphPathTool>("query_graph_path", "Traverse the knowledge graph to find a path between two entities."),'
|
||||
content = content[:list_tools_idx] + tool_def + "\n" + content[list_tools_idx:]
|
||||
|
||||
# Add to match name
|
||||
match_idx = content.find('"create_entities" => {')
|
||||
if match_idx == -1:
|
||||
print("Could not find match name")
|
||||
sys.exit(1)
|
||||
|
||||
handler_code = """ "query_graph_path" => {
|
||||
let req: crate::tools::QueryGraphPathTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Some(crate::mcp::success(req_id, true, &format!("Invalid args: {}", e))),
|
||||
};
|
||||
let graph = state.graph.read();
|
||||
let max_depth = req.max_depth.unwrap_or(5);
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
let mut parents: std::collections::HashMap<String, (String, String)> = std::collections::HashMap::new();
|
||||
|
||||
queue.push_back(req.start_node.clone());
|
||||
visited.insert(req.start_node.clone());
|
||||
|
||||
let mut found = false;
|
||||
let mut current_depth = 0;
|
||||
let mut nodes_at_current_depth = 1;
|
||||
let mut nodes_at_next_depth = 0;
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
if current == req.end_node {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
nodes_at_current_depth -= 1;
|
||||
if current_depth < max_depth {
|
||||
for rel in &graph.relations {
|
||||
if rel.from == current && !visited.contains(&rel.to) {
|
||||
visited.insert(rel.to.clone());
|
||||
parents.insert(rel.to.clone(), (current.clone(), rel.relation_type.clone()));
|
||||
queue.push_back(rel.to.clone());
|
||||
nodes_at_next_depth += 1;
|
||||
} else if rel.to == current && !visited.contains(&rel.from) {
|
||||
visited.insert(rel.from.clone());
|
||||
parents.insert(rel.from.clone(), (current.clone(), format!("inverse({})", rel.relation_type)));
|
||||
queue.push_back(rel.from.clone());
|
||||
nodes_at_next_depth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if nodes_at_current_depth == 0 {
|
||||
current_depth += 1;
|
||||
nodes_at_current_depth = nodes_at_next_depth;
|
||||
nodes_at_next_depth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
let mut path = Vec::new();
|
||||
let mut curr = req.end_node.clone();
|
||||
while curr != req.start_node {
|
||||
let (parent, rel) = parents.get(&curr).unwrap();
|
||||
path.push(format!("({}) --[{}]--> ({})", parent, rel, curr));
|
||||
curr = parent.clone();
|
||||
}
|
||||
path.reverse();
|
||||
Ok(format!("Path found:\\n{}", path.join("\\n")))
|
||||
} else {
|
||||
Ok(format!("No path found between {} and {} within depth {}", req.start_node, req.end_node, max_depth))
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
content = content[:match_idx] + handler_code + content[match_idx:]
|
||||
|
||||
with open(handlers_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Injected query_graph_path tool!")
|
||||
@@ -1,70 +0,0 @@
|
||||
import sys
|
||||
|
||||
main_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\main.rs"
|
||||
with open(main_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Add WebSocket imports
|
||||
import_idx = content.find('use axum::{')
|
||||
if import_idx == -1:
|
||||
print("Could not find axum imports")
|
||||
sys.exit(1)
|
||||
|
||||
import_code = 'use axum::extract::ws::{WebSocketUpgrade, WebSocket, Message};\n'
|
||||
content = content[:import_idx] + import_code + content[import_idx:]
|
||||
|
||||
# 2. Add /ws route
|
||||
route_idx = content.find('.route("/sse", get(sse_handler))')
|
||||
if route_idx == -1:
|
||||
print("Could not find route definition")
|
||||
sys.exit(1)
|
||||
|
||||
route_code = '.route("/ws", get(ws_handler))\n '
|
||||
content = content[:route_idx] + route_code + content[route_idx:]
|
||||
|
||||
# 3. Add ws_handler implementation
|
||||
end_idx = len(content)
|
||||
|
||||
ws_code = """
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_socket(mut socket: WebSocket, state: Arc<AppState>) {
|
||||
let mut rx = state.tx.subscribe();
|
||||
|
||||
// Create a background task to receive messages from the system and send to the WebSocket
|
||||
let mut send_task = tokio::spawn(async move {
|
||||
while let Ok(msg) = rx.recv().await {
|
||||
if socket.send(Message::Text(msg.into())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create a task to receive messages from the WebSocket (e.g. task completion from UI)
|
||||
// In a real app we'd decode JSON-RPC here
|
||||
let mut recv_task = tokio::spawn(async move {
|
||||
// Just keeping it alive and listening
|
||||
// We can add logic to process JSON-RPC from the frontend here later
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = (&mut send_task) => recv_task.abort(),
|
||||
_ = (&mut recv_task) => send_task.abort(),
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
content += ws_code
|
||||
|
||||
with open(main_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Injected WS handler!")
|
||||
@@ -1,59 +0,0 @@
|
||||
import urllib.request
|
||||
import json
|
||||
import threading
|
||||
import sys
|
||||
import time
|
||||
|
||||
sse_url = "http://localhost:3000/sse"
|
||||
|
||||
def call_mcp(method, params):
|
||||
req = urllib.request.Request(sse_url)
|
||||
with urllib.request.urlopen(req) as response:
|
||||
endpoint = None
|
||||
# read the SSE stream
|
||||
for line_bytes in response:
|
||||
line = line_bytes.decode('utf-8').strip()
|
||||
if line.startswith("data: /messages"):
|
||||
endpoint = "http://localhost:3000" + line.split("data: ")[1].strip()
|
||||
break
|
||||
|
||||
if not endpoint:
|
||||
print("Failed to get endpoint")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Got endpoint: {endpoint}")
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": method,
|
||||
"arguments": params
|
||||
}
|
||||
}
|
||||
|
||||
post_req = urllib.request.Request(endpoint, data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json'})
|
||||
with urllib.request.urlopen(post_req) as post_resp:
|
||||
print(f"POST status: {post_resp.status}")
|
||||
|
||||
# continue reading SSE for response
|
||||
for line_bytes in response:
|
||||
line = line_bytes.decode('utf-8').strip()
|
||||
if line.startswith("data: "):
|
||||
data = line[6:]
|
||||
if data == "ping": continue
|
||||
try:
|
||||
msg = json.loads(data)
|
||||
if msg.get("id") == 1:
|
||||
print("RESULT:")
|
||||
print(json.dumps(msg, indent=2))
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
import sys
|
||||
if __name__ == "__main__":
|
||||
method = sys.argv[1]
|
||||
params = json.loads(sys.argv[2])
|
||||
call_mcp(method, params)
|
||||
@@ -1,39 +0,0 @@
|
||||
import re
|
||||
|
||||
endpoints = """
|
||||
.route("/api/graph", get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let graph = state_clone.get_full_graph();
|
||||
axum::Json(graph)
|
||||
}
|
||||
}))
|
||||
.route("/api/tasks", get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let tasks = state_clone.tasks.read();
|
||||
axum::Json(tasks.clone())
|
||||
}
|
||||
}))
|
||||
.route("/api/search", get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>| async move {
|
||||
if let Some(q) = params.get("q") {
|
||||
if let Ok(idx) = state_clone.search_index.read() {
|
||||
let results = idx.search(q, 50);
|
||||
return axum::Json(serde_json::json!({ "results": results }));
|
||||
}
|
||||
}
|
||||
axum::Json(serde_json::json!({ "results": [] }))
|
||||
}
|
||||
}))
|
||||
"""
|
||||
|
||||
filepath = "C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\server\\src\\main.rs"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace('.route("/api/stats",', endpoints + ' .route("/api/stats",')
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
@@ -1,44 +0,0 @@
|
||||
import re
|
||||
|
||||
def fix_handlers(filepath):
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the positions of self.state.write_to_local_delta( and self.state.apply_sync_write(
|
||||
patterns = ["self.state.write_to_local_delta(|", "self.state.apply_sync_write(|"]
|
||||
|
||||
# Actually, we can just find 'write_to_local_delta(|' and 'apply_sync_write(|'
|
||||
|
||||
for method in ["write_to_local_delta", "apply_sync_write"]:
|
||||
start_idx = 0
|
||||
while True:
|
||||
idx = content.find(f"self.state.{method}(|", start_idx)
|
||||
if idx == -1:
|
||||
break
|
||||
|
||||
# Find the closing bracket that matches the first `{` after `|`
|
||||
brace_idx = content.find("{", idx)
|
||||
|
||||
# count braces
|
||||
brace_count = 1
|
||||
curr_idx = brace_idx + 1
|
||||
while brace_count > 0 and curr_idx < len(content):
|
||||
if content[curr_idx] == '{':
|
||||
brace_count += 1
|
||||
elif content[curr_idx] == '}':
|
||||
brace_count -= 1
|
||||
curr_idx += 1
|
||||
|
||||
# curr_idx is now right after the matching '}'
|
||||
# the original code was '});'
|
||||
if content[curr_idx:curr_idx+2] == ");":
|
||||
# we want to insert .await
|
||||
content = content[:curr_idx+1] + ".await" + content[curr_idx+1:]
|
||||
start_idx = curr_idx + 8 # move past `.await);`
|
||||
else:
|
||||
start_idx = curr_idx
|
||||
|
||||
with open(filepath, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
fix_handlers("C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\server\\src\\handlers.rs")
|
||||
@@ -1,78 +0,0 @@
|
||||
import re
|
||||
|
||||
descriptions = {
|
||||
"create_entities": "Create new entities in the knowledge graph.",
|
||||
"create_relations": "Create new relations between entities in the knowledge graph.",
|
||||
"add_observations": "Add new observations to existing entities in the knowledge graph.",
|
||||
"delete_entities": "Delete entities from the knowledge graph.",
|
||||
"delete_observations": "Delete observations from existing entities.",
|
||||
"delete_relations": "Delete relations between entities.",
|
||||
"read_graph": "Read the entire knowledge graph.",
|
||||
"search_nodes": "Search for entities in the knowledge graph by name or type.",
|
||||
"open_nodes": "Open and retrieve full details of specific nodes in the knowledge graph.",
|
||||
"log_code_change": "Log a significant code change or refactor in the memory system.",
|
||||
"query_recent_changes": "Query recently logged code changes.",
|
||||
"visualize_graph": "Generate a visual representation of the knowledge graph.",
|
||||
"add_sticky_note": "Add a sticky note for unstructured thoughts or reminders.",
|
||||
"read_sticky_notes": "Read all active sticky notes.",
|
||||
"condense_entity": "Condense or summarize an entity's observations to reduce size.",
|
||||
"add_task": "Add a new task to the task tracker.",
|
||||
"update_task_status": "Update the status of an existing task.",
|
||||
"list_active_tasks": "List all currently active tasks.",
|
||||
"store_snippet": "Store a reusable code snippet.",
|
||||
"search_snippets": "Search through stored code snippets.",
|
||||
"delete_snippet": "Delete a stored code snippet.",
|
||||
"log_decision": "Log an architectural decision record (ADR).",
|
||||
"query_decisions": "Query architectural decision records.",
|
||||
"merge_entities": "Merge two entities in the knowledge graph into one.",
|
||||
"find_orphans": "Find orphaned entities (entities without any relations) in the graph.",
|
||||
"learn_preference": "Record a user preference or behavior to adapt future interactions.",
|
||||
"read_preferences": "Read all learned user preferences.",
|
||||
"log_error_fix": "Log a complex error and its fix for future reference.",
|
||||
"search_error_fixes": "Search through previously logged error fixes.",
|
||||
"pin_file": "Pin a file to keep it explicitly in the context workspace.",
|
||||
"unpin_file": "Unpin a file from the context workspace.",
|
||||
"list_pinned_files": "List all currently pinned files.",
|
||||
"add_session_summary": "Add a summary of the current session.",
|
||||
"get_project_timeline": "Get a timeline of major project events.",
|
||||
"leave_handoff_memo": "Leave a memo for the next session or agent.",
|
||||
"read_handoff_memos": "Read pending handoff memos.",
|
||||
"clear_handoff_memos": "Clear handoff memos after reading.",
|
||||
"update_env_fingerprint": "Update the environment fingerprint (e.g., OS, tool versions).",
|
||||
"read_env_fingerprint": "Read the current environment fingerprint.",
|
||||
"log_env_requirement": "Log a required tool or package for the environment.",
|
||||
"add_milestone": "Add a new project milestone.",
|
||||
"update_milestone": "Update the status of a project milestone.",
|
||||
"list_milestones": "List all project milestones.",
|
||||
"generate_standup_report": "Generate a standup report based on recent activity.",
|
||||
"register_environment": "Register details about a specific deployment environment.",
|
||||
"get_environment_details": "Get details about a registered environment.",
|
||||
"add_pr_checklist_item": "Add an item to the PR checklist.",
|
||||
"get_pr_checklist": "Get the current PR checklist.",
|
||||
"clear_pr_checklist": "Clear the PR checklist.",
|
||||
"log_tech_debt": "Log identified technical debt.",
|
||||
"resolve_tech_debt": "Mark a logged technical debt as resolved.",
|
||||
"list_tech_debt": "List all unresolved technical debt.",
|
||||
"save_context_workspace": "Save the current set of pinned files and context.",
|
||||
"load_context_workspace": "Load a previously saved context workspace.",
|
||||
"list_context_workspaces": "List all saved context workspaces.",
|
||||
"omni_search": "Search across all memory sources (graph, tasks, snippets, ADRs, etc.) at once.",
|
||||
"get_project_health": "Get a synthesized health report of the project based on memory data."
|
||||
}
|
||||
|
||||
def fix_handlers(filepath):
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
for tool_name, description in descriptions.items():
|
||||
# Match crate::mcp::tool_def::<...>("tool_name", "")
|
||||
# Note: sometimes it's on multiple lines for GenerateStandupReportTool etc.
|
||||
# But most are single line.
|
||||
pattern = r'(crate::mcp::tool_def::<[^>]+>\(\s*"' + tool_name + r'"\s*,\s*)""(\s*\))'
|
||||
replacement = r'\1"' + description + r'"\2'
|
||||
content = re.sub(pattern, replacement, content)
|
||||
|
||||
with open(filepath, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
fix_handlers("C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\server\\src\\handlers.rs")
|
||||
@@ -1,50 +0,0 @@
|
||||
import re
|
||||
|
||||
endpoints = """
|
||||
.route("/api/graph", get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let graph = state_clone.get_full_graph();
|
||||
axum::Json(graph)
|
||||
}
|
||||
}))
|
||||
.route("/api/tasks", get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let tasks = state_clone.tasks.read();
|
||||
axum::Json(tasks.clone())
|
||||
}
|
||||
}))
|
||||
.route("/api/search", get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>| async move {
|
||||
if let Some(q) = params.get("q") {
|
||||
if let Ok(idx) = state_clone.search_index.read() {
|
||||
if let Ok(results) = idx.search(q, None) {
|
||||
let mut formatted_results = Vec::new();
|
||||
for (type_name, content) in results {
|
||||
formatted_results.push(serde_json::json!({
|
||||
"type_name": type_name,
|
||||
"content": content,
|
||||
"score": 1.0
|
||||
}));
|
||||
}
|
||||
return axum::Json(serde_json::json!({ "results": formatted_results }));
|
||||
}
|
||||
}
|
||||
}
|
||||
axum::Json(serde_json::json!({ "results": [] }))
|
||||
}
|
||||
}))
|
||||
"""
|
||||
|
||||
filepath = "C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\server\\src\\main.rs"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace the broken routes with the fixed ones
|
||||
pattern = r'\.route\("/api/graph".*?\.route\("/api/stats"'
|
||||
content = re.sub(pattern, endpoints.strip() + '\n .route("/api/stats"', content, flags=re.DOTALL)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
+70
-481
@@ -2,6 +2,22 @@ use crate::models::*;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::*;
|
||||
|
||||
macro_rules! parse_tool {
|
||||
($args:expr, $id:expr, $type:ty) => {
|
||||
match parse_args::<$type>($args) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
$id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": format!("Invalid args: {}", e)}] }),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
@@ -124,10 +140,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
|
||||
let result: Result<String, String> = match name {
|
||||
"query_graph_path" => {
|
||||
let req: crate::tools::QueryGraphPathTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Some(crate::mcp::success(id.clone(), serde_json::json!({"isError": true, "content": [{"type": "text", "text": format!("Invalid args: {}", e)}] })))
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, crate::tools::QueryGraphPathTool);
|
||||
let graph = self.state.get_full_graph();
|
||||
let max_depth = req.max_depth.unwrap_or(5);
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
@@ -185,15 +198,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
}
|
||||
}
|
||||
"create_entities" => {
|
||||
let req: CreateEntitiesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, CreateEntitiesTool);
|
||||
self.state.write_to_local_delta(|g| {
|
||||
for entity in req.entities {
|
||||
if !entity.name.is_empty() {
|
||||
@@ -207,15 +212,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Entities created".to_string()][0].clone())
|
||||
}
|
||||
"create_relations" => {
|
||||
let req: CreateRelationsTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, CreateRelationsTool);
|
||||
self.state.write_to_local_delta(|g| {
|
||||
for relation in req.relations {
|
||||
if !relation.from.is_empty() && !relation.to.is_empty() {
|
||||
@@ -226,15 +223,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Relations created".to_string()][0].clone())
|
||||
}
|
||||
"add_observations" => {
|
||||
let req: AddObservationsTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, AddObservationsTool);
|
||||
let full = self.state.get_full_graph();
|
||||
self.state.write_to_local_delta(|g| {
|
||||
for o in req.observations {
|
||||
@@ -258,15 +247,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Observations added".to_string()][0].clone())
|
||||
}
|
||||
"delete_entities" => {
|
||||
let req: DeleteEntitiesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, DeleteEntitiesTool);
|
||||
let to_delete: HashSet<_> = req.entity_names.into_iter().collect();
|
||||
self.state.apply_sync_write(|master| {
|
||||
for name in &to_delete {
|
||||
@@ -279,15 +260,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Entities deleted".to_string()][0].clone())
|
||||
}
|
||||
"delete_observations" => {
|
||||
let req: DeleteObservationsTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, DeleteObservationsTool);
|
||||
self.state.apply_sync_write(|master| {
|
||||
for d in req.deletions {
|
||||
if let Some(e) = master.entities.get_mut(&d.entity_name)
|
||||
@@ -300,15 +273,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Observations deleted".to_string()][0].clone())
|
||||
}
|
||||
"delete_relations" => {
|
||||
let req: DeleteRelationsTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, DeleteRelationsTool);
|
||||
self.state.apply_sync_write(|master| {
|
||||
let mut to_rem = HashSet::new();
|
||||
for r in req.relations {
|
||||
@@ -327,15 +292,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Relations deleted".to_string()][0].clone())
|
||||
}
|
||||
"read_graph" => {
|
||||
let req: ReadGraphTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ReadGraphTool);
|
||||
let mut full = self.state.get_full_graph();
|
||||
if let Some(ns) = req.namespace {
|
||||
full.entities.retain(|_, e| e.namespace == ns);
|
||||
@@ -345,15 +302,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"search_nodes" => {
|
||||
let req: SearchNodesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, SearchNodesTool);
|
||||
let matches = if let Ok(idx) = self.state.search_index.read() {
|
||||
idx.search(&req.query, req.namespace.as_deref())
|
||||
.unwrap_or_default()
|
||||
@@ -373,15 +322,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"open_nodes" => {
|
||||
let req: OpenNodesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, OpenNodesTool);
|
||||
let targets: HashSet<_> = req.names.into_iter().collect();
|
||||
let full = self.state.get_full_graph();
|
||||
let mut result = KnowledgeGraph::default();
|
||||
@@ -404,15 +345,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"log_code_change" => {
|
||||
let req: LogCodeChangeTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LogCodeChangeTool);
|
||||
self.state.ledger.modify(|ledger| {
|
||||
ledger.push(CodeChange {
|
||||
timestamp: SystemTime::now()
|
||||
@@ -433,15 +366,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"visualize_graph" => {
|
||||
let req: VisualizeGraphTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, VisualizeGraphTool);
|
||||
let query = req.query.unwrap_or_default().to_lowercase();
|
||||
let full = self.state.get_full_graph();
|
||||
let mut included = HashSet::new();
|
||||
@@ -511,15 +436,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![output.to_string()][0].clone())
|
||||
}
|
||||
"add_sticky_note" => {
|
||||
let req: AddStickyNoteTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, AddStickyNoteTool);
|
||||
self.state.sticky.modify(|notes| {
|
||||
notes.push(StickyNote {
|
||||
timestamp: SystemTime::now()
|
||||
@@ -537,15 +454,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"condense_entity" => {
|
||||
let req: CondenseEntityTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, CondenseEntityTool);
|
||||
self.state.apply_sync_write(|master| {
|
||||
if let Some(e) = master.entities.get_mut(&req.entity_name) {
|
||||
e.observations = req.summarized_observations;
|
||||
@@ -554,15 +463,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Entity condensed".to_string()][0].clone())
|
||||
}
|
||||
"add_task" => {
|
||||
let req: AddTaskTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, AddTaskTool);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -586,15 +487,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![format!("Task added with ID: {}", id).to_string()][0].clone())
|
||||
}
|
||||
"update_task_status" => {
|
||||
let req: UpdateTaskStatusTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, UpdateTaskStatusTool);
|
||||
let mut found = false;
|
||||
self.state.tasks.modify(|tasks| {
|
||||
for t in tasks.iter_mut() {
|
||||
@@ -616,15 +509,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
}
|
||||
}
|
||||
"list_active_tasks" => {
|
||||
let req: ListActiveTasksTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ListActiveTasksTool);
|
||||
let mut tasks = self.state.tasks.read();
|
||||
tasks.retain(|t| t.status != "done");
|
||||
if let Some(branch) = req.git_branch {
|
||||
@@ -637,15 +522,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"store_snippet" => {
|
||||
let req: StoreSnippetTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, StoreSnippetTool);
|
||||
self.state.snippets.modify(|snippets| {
|
||||
snippets.retain(|s| s.name != req.name);
|
||||
snippets.push(Snippet {
|
||||
@@ -662,15 +539,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![format!("Snippet '{}' stored.", req.name).to_string()][0].clone())
|
||||
}
|
||||
"search_snippets" => {
|
||||
let req: SearchSnippetsTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, SearchSnippetsTool);
|
||||
let query = req.query.to_lowercase();
|
||||
let snippets = self.state.snippets.read();
|
||||
let mut results = Vec::new();
|
||||
@@ -686,15 +555,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"delete_snippet" => {
|
||||
let req: DeleteSnippetTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, DeleteSnippetTool);
|
||||
let mut deleted = false;
|
||||
self.state.snippets.modify(|snippets| {
|
||||
let orig = snippets.len();
|
||||
@@ -708,15 +569,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
}
|
||||
}
|
||||
"log_decision" => {
|
||||
let req: LogDecisionTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LogDecisionTool);
|
||||
let mut id = String::new();
|
||||
self.state.adrs.modify(|adrs| {
|
||||
id = format!("ADR-{:04}", adrs.len() + 1);
|
||||
@@ -735,15 +588,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![format!("Decision logged as {}", id).to_string()][0].clone())
|
||||
}
|
||||
"query_decisions" => {
|
||||
let req: QueryDecisionsTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, QueryDecisionsTool);
|
||||
let mut adrs = self.state.adrs.read();
|
||||
if let Some(q) = req.query {
|
||||
let q = q.to_lowercase();
|
||||
@@ -757,15 +602,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"merge_entities" => {
|
||||
let req: MergeEntitiesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, MergeEntitiesTool);
|
||||
self.state.apply_sync_write(|master| {
|
||||
if let Some(src) = master.entities.remove(&req.source_entity) {
|
||||
if let Some(tgt) = master.entities.get_mut(&req.target_entity) {
|
||||
@@ -807,15 +644,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"learn_preference" => {
|
||||
let req: LearnPreferenceTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LearnPreferenceTool);
|
||||
self.state.prefs.modify(|prefs| {
|
||||
prefs.insert(
|
||||
req.key.clone(),
|
||||
@@ -837,15 +666,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"log_error_fix" => {
|
||||
let req: LogErrorFixTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LogErrorFixTool);
|
||||
self.state.error_fixes.modify(|fixes| {
|
||||
fixes.push(crate::models::ErrorFix {
|
||||
signature: req.signature,
|
||||
@@ -861,15 +682,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Error fix logged".to_string()][0].clone())
|
||||
}
|
||||
"search_error_fixes" => {
|
||||
let req: SearchErrorFixesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, SearchErrorFixesTool);
|
||||
let q = req.query.to_lowercase();
|
||||
let mut fixes = self.state.error_fixes.read();
|
||||
fixes.retain(|f| {
|
||||
@@ -880,15 +693,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"pin_file" => {
|
||||
let req: PinFileTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, PinFileTool);
|
||||
self.state.pinned_files.modify(|pinned| {
|
||||
pinned.retain(|p| {
|
||||
!(p.namespace == req.namespace && p.file_path == req.file_path)
|
||||
@@ -906,15 +711,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["File pinned".to_string()][0].clone())
|
||||
}
|
||||
"unpin_file" => {
|
||||
let req: UnpinFileTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, UnpinFileTool);
|
||||
self.state.pinned_files.modify(|pinned| {
|
||||
pinned.retain(|p| {
|
||||
!(p.namespace == req.namespace && p.file_path == req.file_path)
|
||||
@@ -923,15 +720,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["File unpinned".to_string()][0].clone())
|
||||
}
|
||||
"list_pinned_files" => {
|
||||
let req: ListPinnedFilesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ListPinnedFilesTool);
|
||||
let mut pinned = self.state.pinned_files.read();
|
||||
if let Some(ns) = req.namespace {
|
||||
pinned.retain(|p| p.namespace == ns);
|
||||
@@ -946,15 +735,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"add_session_summary" => {
|
||||
let req: AddSessionSummaryTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, AddSessionSummaryTool);
|
||||
self.state.session_summaries.modify(|summaries| {
|
||||
summaries.push(crate::models::SessionSummary {
|
||||
summary: req.summary,
|
||||
@@ -968,15 +749,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Session summary added".to_string()][0].clone())
|
||||
}
|
||||
"get_project_timeline" => {
|
||||
let req: GetProjectTimelineTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, GetProjectTimelineTool);
|
||||
let mut summaries = self.state.session_summaries.read();
|
||||
if let Some(ns) = req.namespace {
|
||||
summaries.retain(|s| s.namespace == ns);
|
||||
@@ -986,15 +759,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"leave_handoff_memo" => {
|
||||
let req: LeaveHandoffMemoTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LeaveHandoffMemoTool);
|
||||
self.state.handoff_memos.modify(|memos| {
|
||||
memos.push(crate::models::HandoffMemo {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
@@ -1010,15 +775,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Handoff memo left".to_string()][0].clone())
|
||||
}
|
||||
"read_handoff_memos" => {
|
||||
let req: ReadHandoffMemosTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ReadHandoffMemosTool);
|
||||
let mut memos = self.state.handoff_memos.read();
|
||||
if let Some(ns) = req.namespace {
|
||||
memos.retain(|m| m.namespace == ns);
|
||||
@@ -1027,15 +784,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"clear_handoff_memos" => {
|
||||
let req: ClearHandoffMemosTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ClearHandoffMemosTool);
|
||||
let ids: HashSet<_> = req.ids.into_iter().collect();
|
||||
self.state
|
||||
.handoff_memos
|
||||
@@ -1043,15 +792,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Handoff memos cleared".to_string()][0].clone())
|
||||
}
|
||||
"update_env_fingerprint" => {
|
||||
let req: UpdateEnvFingerprintTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, UpdateEnvFingerprintTool);
|
||||
self.state.env_fingerprints.modify(|fps| {
|
||||
fps.insert(
|
||||
req.namespace.clone(),
|
||||
@@ -1071,15 +812,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Env fingerprint updated".to_string()][0].clone())
|
||||
}
|
||||
"read_env_fingerprint" => {
|
||||
let req: ReadEnvFingerprintTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ReadEnvFingerprintTool);
|
||||
let fps = self.state.env_fingerprints.read();
|
||||
if let Some(fp) = fps.get(&req.namespace) {
|
||||
let data = serde_json::to_string(fp).unwrap_or_default();
|
||||
@@ -1089,15 +822,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
}
|
||||
}
|
||||
"log_env_requirement" => {
|
||||
let req: LogEnvRequirementTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LogEnvRequirementTool);
|
||||
self.state.env_requirements.modify(|reqs| {
|
||||
reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key));
|
||||
reqs.push(crate::models::EnvRequirement {
|
||||
@@ -1110,15 +835,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Env requirement logged".to_string()][0].clone())
|
||||
}
|
||||
"add_milestone" => {
|
||||
let req: AddMilestoneTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, AddMilestoneTool);
|
||||
self.state.milestones.modify(|ms| {
|
||||
ms.push(crate::models::Milestone {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
@@ -1131,15 +848,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Milestone added".to_string()][0].clone())
|
||||
}
|
||||
"update_milestone" => {
|
||||
let req: UpdateMilestoneTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, UpdateMilestoneTool);
|
||||
let mut found = false;
|
||||
self.state.milestones.modify(|ms| {
|
||||
for m in ms.iter_mut() {
|
||||
@@ -1157,15 +866,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
}
|
||||
}
|
||||
"list_milestones" => {
|
||||
let req: ListMilestonesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ListMilestonesTool);
|
||||
let mut ms = self.state.milestones.read();
|
||||
if let Some(ns) = req.namespace {
|
||||
ms.retain(|m| m.namespace == ns);
|
||||
@@ -1174,15 +875,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"generate_standup_report" => {
|
||||
let req: GenerateStandupReportTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, GenerateStandupReportTool);
|
||||
let cutoff = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -1213,15 +906,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![report.to_string().to_string()][0].clone())
|
||||
}
|
||||
"register_environment" => {
|
||||
let req: RegisterEnvironmentTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, RegisterEnvironmentTool);
|
||||
self.state.environments.modify(|envs| {
|
||||
envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name));
|
||||
envs.push(crate::models::EnvironmentDetail {
|
||||
@@ -1239,30 +924,14 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Environment registered".to_string()][0].clone())
|
||||
}
|
||||
"get_environment_details" => {
|
||||
let req: GetEnvironmentDetailsTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, GetEnvironmentDetailsTool);
|
||||
let mut envs = self.state.environments.read();
|
||||
envs.retain(|e| e.namespace == req.namespace);
|
||||
let data = serde_json::to_string(&envs).unwrap_or_default();
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"add_pr_checklist_item" => {
|
||||
let req: AddPrChecklistItemTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, AddPrChecklistItemTool);
|
||||
self.state.pr_checklists.modify(|items| {
|
||||
items.push(crate::models::PrChecklistItem {
|
||||
namespace: req.namespace,
|
||||
@@ -1273,45 +942,21 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["PR checklist item added".to_string()][0].clone())
|
||||
}
|
||||
"get_pr_checklist" => {
|
||||
let req: GetPrChecklistTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, GetPrChecklistTool);
|
||||
let mut items = self.state.pr_checklists.read();
|
||||
items.retain(|i| i.namespace == req.namespace);
|
||||
let data = serde_json::to_string(&items).unwrap_or_default();
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"clear_pr_checklist" => {
|
||||
let req: ClearPrChecklistTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ClearPrChecklistTool);
|
||||
self.state
|
||||
.pr_checklists
|
||||
.modify(|items| items.retain(|i| i.namespace != req.namespace));
|
||||
Ok(vec!["PR checklist cleared".to_string()][0].clone())
|
||||
}
|
||||
"log_tech_debt" => {
|
||||
let req: LogTechDebtTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LogTechDebtTool);
|
||||
self.state.tech_debts.modify(|debts| {
|
||||
debts.push(crate::models::TechDebt {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
@@ -1330,15 +975,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Tech debt logged".to_string()][0].clone())
|
||||
}
|
||||
"resolve_tech_debt" => {
|
||||
let req: ResolveTechDebtTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ResolveTechDebtTool);
|
||||
let mut found = false;
|
||||
self.state.tech_debts.modify(|debts| {
|
||||
for d in debts.iter_mut() {
|
||||
@@ -1356,15 +993,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
}
|
||||
}
|
||||
"list_tech_debt" => {
|
||||
let req: ListTechDebtTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ListTechDebtTool);
|
||||
let mut debts = self.state.tech_debts.read();
|
||||
debts.retain(|d| {
|
||||
d.namespace == req.namespace && (req.include_resolved || !d.is_resolved)
|
||||
@@ -1373,15 +1002,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"save_context_workspace" => {
|
||||
let req: SaveContextWorkspaceTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, SaveContextWorkspaceTool);
|
||||
self.state.context_workspaces.modify(|ws| {
|
||||
ws.retain(|w| !(w.namespace == req.namespace && w.name == req.name));
|
||||
ws.push(crate::models::ContextWorkspace {
|
||||
@@ -1398,45 +1019,21 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec!["Context workspace saved".to_string()][0].clone())
|
||||
}
|
||||
"load_context_workspace" => {
|
||||
let req: LoadContextWorkspaceTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, LoadContextWorkspaceTool);
|
||||
let mut ws = self.state.context_workspaces.read();
|
||||
ws.retain(|w| w.namespace == req.namespace && w.name == req.name);
|
||||
let data = serde_json::to_string(&ws.first()).unwrap_or_default();
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"list_context_workspaces" => {
|
||||
let req: ListContextWorkspacesTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, ListContextWorkspacesTool);
|
||||
let mut ws = self.state.context_workspaces.read();
|
||||
ws.retain(|w| w.namespace == req.namespace);
|
||||
let data = serde_json::to_string(&ws).unwrap_or_default();
|
||||
Ok(vec![data.to_string()][0].clone())
|
||||
}
|
||||
"omni_search" => {
|
||||
let req: OmniSearchTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, OmniSearchTool);
|
||||
let matches = if let Ok(idx) = self.state.search_index.read() {
|
||||
idx.search(&req.query, req.namespace.as_deref())
|
||||
.unwrap_or_default()
|
||||
@@ -1522,15 +1119,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
|
||||
Ok(vec![report.to_string().to_string()][0].clone())
|
||||
}
|
||||
"get_project_health" => {
|
||||
let req: GetProjectHealthTool = match parse_args(args.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(crate::mcp::success(
|
||||
id.clone(),
|
||||
serde_json::json!({"isError": true, "content": [{"type": "text", "text": e}] }),
|
||||
));
|
||||
}
|
||||
};
|
||||
let req = parse_tool!(args.clone(), id, GetProjectHealthTool);
|
||||
let active_tasks = self
|
||||
.state
|
||||
.tasks
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import sys
|
||||
|
||||
cargo_path = r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\Cargo.toml"
|
||||
with open(cargo_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Swap reqwest rustls-tls in instead of default (which uses native-tls / openssl)
|
||||
# This entirely removes the openssl dependency during Linux cross-compilation
|
||||
if 'reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }' not in content:
|
||||
content = content.replace('reqwest = "0.12"', 'reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }')
|
||||
|
||||
# For git2, we don't need vendored openssl if we're not using https fetch/push for libgit2.
|
||||
# We just need to read local `.git` files, so we can disable the default features.
|
||||
if 'git2 = { version = "0.19.0", features = ["vendored-openssl"] }' in content:
|
||||
content = content.replace('git2 = { version = "0.19.0", features = ["vendored-openssl"] }', 'git2 = { version = "0.19.0", default-features = false }')
|
||||
elif 'git2 = "0.19.0"' in content:
|
||||
content = content.replace('git2 = "0.19.0"', 'git2 = { version = "0.19.0", default-features = false }')
|
||||
|
||||
with open(cargo_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("Removed OpenSSL dependency entirely from server Cargo.toml")
|
||||
@@ -82,7 +82,7 @@ fn test_mcp_initialization_and_tools_list() {
|
||||
|
||||
// Verify capabilities
|
||||
let capabilities = &init_resp["result"]["capabilities"];
|
||||
assert!(capabilities.get("tools").is_none(), "capabilities.tools MUST NOT be present in initialize response to avoid crashing Antigravity proxy!");
|
||||
assert_eq!(capabilities["tools"], serde_json::json!({}));
|
||||
|
||||
// 2. Test tools/list
|
||||
let tools_req = json!({
|
||||
|
||||
Reference in new issue
Block a user