fix(server): safely drop detached tokio join handles and refactor timestamp boilerplate
This commit is contained in:
1 parent
13c7eca4c2
commit
478655698e
14 files changed
+442
-297
No files matched your search
@@ -38,4 +38,3 @@ pub async fn read_mcp_message<R: tokio::io::AsyncRead + Unpin>(
|
|||||||
|
|
||||||
String::from_utf8(buffer).ok()
|
String::from_utf8(buffer).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
+94
-37
@@ -199,8 +199,10 @@ async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
|||||||
let msgid = &arr[1];
|
let msgid = &arr[1];
|
||||||
let msgid_str = format!("{msgid:?}");
|
let msgid_str = format!("{msgid:?}");
|
||||||
|
|
||||||
if let Some(reply_sender) =
|
if let Some(reply_sender) = pending_clone2
|
||||||
pending_clone2.lock().unwrap_or_else(|e| e.into_inner()).remove(&msgid_str)
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.remove(&msgid_str)
|
||||||
{
|
{
|
||||||
let _ = reply_sender.send(Ok(val));
|
let _ = reply_sender.send(Ok(val));
|
||||||
}
|
}
|
||||||
@@ -555,7 +557,9 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
tracing::info!("{} MCP server started", app_name);
|
tracing::info!("{} MCP server started", app_name);
|
||||||
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
|
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
|
||||||
loop {
|
loop {
|
||||||
let raw_msg = if let Some(m) = mcp_stdio::read_mcp_message(&mut stdin).await { m } else {
|
let raw_msg = if let Some(m) = mcp_stdio::read_mcp_message(&mut stdin).await {
|
||||||
|
m
|
||||||
|
} else {
|
||||||
tracing::info!("Stdin closed, exiting loop");
|
tracing::info!("Stdin closed, exiting loop");
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
@@ -595,7 +599,6 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match msg.method.as_str() {
|
match msg.method.as_str() {
|
||||||
|
|
||||||
"initialize" => {
|
"initialize" => {
|
||||||
let init = rmcp::model::InitializeResult::new(
|
let init = rmcp::model::InitializeResult::new(
|
||||||
rmcp::model::ServerCapabilities::builder()
|
rmcp::model::ServerCapabilities::builder()
|
||||||
@@ -719,7 +722,13 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
let cmd = format!("e {escaped_file} | {line} | normal! zz");
|
let cmd = format!("e {escaped_file} | {line} | normal! zz");
|
||||||
match send_nvim_command(&cmd).await {
|
match send_nvim_command(&cmd).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
send_text_result!(id.clone(), format!("Successfully jumped to {} line {}", file, line));
|
send_text_result!(
|
||||||
|
id.clone(),
|
||||||
|
format!(
|
||||||
|
"Successfully jumped to {} line {}",
|
||||||
|
file, line
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -752,7 +761,10 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
) {
|
) {
|
||||||
match set_nvim_diagnostics(line, message).await {
|
match set_nvim_diagnostics(line, message).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
send_text_result!(id.clone(), format!("Successfully set diagnostic on line {}", line));
|
send_text_result!(
|
||||||
|
id.clone(),
|
||||||
|
format!("Successfully set diagnostic on line {}", line)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
@@ -804,23 +816,32 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"nvim_open_file" => {
|
"nvim_open_file" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
let code = format!("
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
|
let code = format!(
|
||||||
|
"
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
vim.cmd('edit ' .. vim.fn.fnameescape(args.file))
|
vim.cmd('edit ' .. vim.fn.fnameescape(args.file))
|
||||||
if args.filetype and args.filetype ~= '' then
|
if args.filetype and args.filetype ~= '' then
|
||||||
vim.bo.filetype = args.filetype
|
vim.bo.filetype = args.filetype
|
||||||
end
|
end
|
||||||
return 'Opened file ' .. args.file
|
return 'Opened file ' .. args.file
|
||||||
");
|
"
|
||||||
|
);
|
||||||
match execute_nvim_lua(&code).await {
|
match execute_nvim_lua(&code).await {
|
||||||
Ok(res) => send_text_result!(id.clone(), res),
|
Ok(res) => send_text_result!(id.clone(), res),
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_open_buffer" => {
|
"nvim_open_buffer" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
let code = format!("
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
|
let code = format!(
|
||||||
|
"
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
local buf = vim.api.nvim_create_buf(true, true)
|
local buf = vim.api.nvim_create_buf(true, true)
|
||||||
if args.name and args.name ~= '' then
|
if args.name and args.name ~= '' then
|
||||||
@@ -835,29 +856,39 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
end
|
end
|
||||||
vim.api.nvim_win_set_buf(0, buf)
|
vim.api.nvim_win_set_buf(0, buf)
|
||||||
return 'Opened buffer ' .. tostring(buf)
|
return 'Opened buffer ' .. tostring(buf)
|
||||||
");
|
"
|
||||||
|
);
|
||||||
match execute_nvim_lua(&code).await {
|
match execute_nvim_lua(&code).await {
|
||||||
Ok(res) => send_text_result!(id.clone(), res),
|
Ok(res) => send_text_result!(id.clone(), res),
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_close_buffer" => {
|
"nvim_close_buffer" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
let code = format!("
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
|
let code = format!(
|
||||||
|
"
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
local buf = args.buf_id or vim.api.nvim_get_current_buf()
|
local buf = args.buf_id or vim.api.nvim_get_current_buf()
|
||||||
local force = args.force or false
|
local force = args.force or false
|
||||||
vim.api.nvim_buf_delete(buf, {{ force = force }})
|
vim.api.nvim_buf_delete(buf, {{ force = force }})
|
||||||
return 'Closed buffer ' .. tostring(buf)
|
return 'Closed buffer ' .. tostring(buf)
|
||||||
");
|
"
|
||||||
|
);
|
||||||
match execute_nvim_lua(&code).await {
|
match execute_nvim_lua(&code).await {
|
||||||
Ok(res) => send_text_result!(id.clone(), res),
|
Ok(res) => send_text_result!(id.clone(), res),
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_split_window" => {
|
"nvim_split_window" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
let code = format!("
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
|
let code = format!(
|
||||||
|
"
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
local cmd = args.direction == 'horizontal' and 'split' or 'vsplit'
|
local cmd = args.direction == 'horizontal' and 'split' or 'vsplit'
|
||||||
vim.cmd(cmd)
|
vim.cmd(cmd)
|
||||||
@@ -867,15 +898,20 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
vim.api.nvim_win_set_buf(0, args.buf_id)
|
vim.api.nvim_win_set_buf(0, args.buf_id)
|
||||||
end
|
end
|
||||||
return 'Split window created'
|
return 'Split window created'
|
||||||
");
|
"
|
||||||
|
);
|
||||||
match execute_nvim_lua(&code).await {
|
match execute_nvim_lua(&code).await {
|
||||||
Ok(res) => send_text_result!(id.clone(), res),
|
Ok(res) => send_text_result!(id.clone(), res),
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_reload_buffer" => {
|
"nvim_reload_buffer" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
let code = format!("
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
|
let code = format!(
|
||||||
|
"
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
local buf = args.buf_id or vim.api.nvim_get_current_buf()
|
local buf = args.buf_id or vim.api.nvim_get_current_buf()
|
||||||
vim.api.nvim_buf_call(buf, function()
|
vim.api.nvim_buf_call(buf, function()
|
||||||
@@ -886,7 +922,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
return 'Reloaded buffer ' .. tostring(buf)
|
return 'Reloaded buffer ' .. tostring(buf)
|
||||||
");
|
"
|
||||||
|
);
|
||||||
match execute_nvim_lua(&code).await {
|
match execute_nvim_lua(&code).await {
|
||||||
Ok(res) => send_text_result!(id.clone(), res),
|
Ok(res) => send_text_result!(id.clone(), res),
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
@@ -903,8 +940,12 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_set_quickfix" => {
|
"nvim_set_quickfix" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
let code = format!("
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
|
let code = format!(
|
||||||
|
"
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
local items = args.items or {{}}
|
local items = args.items or {{}}
|
||||||
local action = ' '
|
local action = ' '
|
||||||
@@ -914,14 +955,18 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
vim.fn.setqflist(items, action)
|
vim.fn.setqflist(items, action)
|
||||||
vim.cmd('copen')
|
vim.cmd('copen')
|
||||||
return 'Populated quickfix with ' .. tostring(#items) .. ' items'
|
return 'Populated quickfix with ' .. tostring(#items) .. ' items'
|
||||||
");
|
"
|
||||||
|
);
|
||||||
match execute_nvim_lua(&code).await {
|
match execute_nvim_lua(&code).await {
|
||||||
Ok(res) => send_text_result!(id.clone(), res),
|
Ok(res) => send_text_result!(id.clone(), res),
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_highlight_lines" => {
|
"nvim_highlight_lines" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
let code = format!("
|
let code = format!("
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
local buf = args.buf_id or vim.api.nvim_get_current_buf()
|
local buf = args.buf_id or vim.api.nvim_get_current_buf()
|
||||||
@@ -952,8 +997,12 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nvim_get_messages" => {
|
"nvim_get_messages" => {
|
||||||
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()).replace('\\', "\\\\").replace('\'', "\\'");
|
let json_str = serde_json::to_string(args)
|
||||||
let code = format!("
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\'', "\\'");
|
||||||
|
let code = format!(
|
||||||
|
"
|
||||||
local args = vim.json.decode('{json_str}')
|
local args = vim.json.decode('{json_str}')
|
||||||
local msg = vim.fn.execute('messages')
|
local msg = vim.fn.execute('messages')
|
||||||
local lines = vim.split(msg, '\\n')
|
local lines = vim.split(msg, '\\n')
|
||||||
@@ -965,7 +1014,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
return table.concat(tail_lines, '\\n')
|
return table.concat(tail_lines, '\\n')
|
||||||
end
|
end
|
||||||
return msg
|
return msg
|
||||||
");
|
"
|
||||||
|
);
|
||||||
match execute_nvim_lua(&code).await {
|
match execute_nvim_lua(&code).await {
|
||||||
Ok(res) => send_text_result!(id.clone(), res),
|
Ok(res) => send_text_result!(id.clone(), res),
|
||||||
Err(e) => send_error(id, -32603, &e).await,
|
Err(e) => send_error(id, -32603, &e).await,
|
||||||
@@ -992,14 +1042,26 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
|
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
|
||||||
// BAKE IN: Block interactive prompts that cause server deadlocks
|
// BAKE IN: Block interactive prompts that cause server deadlocks
|
||||||
let lower_code = code.to_lowercase();
|
let lower_code = code.to_lowercase();
|
||||||
if lower_code.contains("vim.fn.input") || lower_code.contains("vim.ui.select") || lower_code.contains("vim.fn.confirm") || lower_code.contains("vim.ui.input") {
|
if lower_code.contains("vim.fn.input")
|
||||||
|
|| lower_code.contains("vim.ui.select")
|
||||||
|
|| lower_code.contains("vim.fn.confirm")
|
||||||
|
|| lower_code.contains("vim.ui.input")
|
||||||
|
{
|
||||||
send_error(id, -32600, "CRITICAL ERROR: Interactive Lua functions (input, select, confirm) are strictly forbidden via MCP as they will hang the headless server.").await;
|
send_error(id, -32600, "CRITICAL ERROR: Interactive Lua functions (input, select, confirm) are strictly forbidden via MCP as they will hang the headless server.").await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Block the 'c' confirm flag in vim.cmd substitutions
|
// Block the 'c' confirm flag in vim.cmd substitutions
|
||||||
if (code.contains("vim.cmd") || code.contains("vim.api.nvim_command")) && code.contains("%s") && (code.contains("gc'") || code.contains("gc\"") || code.contains("gc\n") || code.contains("c'") || code.contains("c\"")) {
|
if (code.contains("vim.cmd")
|
||||||
send_error(id, -32600, "CRITICAL ERROR: The 'c' (confirm) flag in Neovim substitutions is strictly forbidden via MCP as it triggers an interactive prompt that hangs the headless server. Use '/g' or '/ge' instead.").await;
|
|| code.contains("vim.api.nvim_command"))
|
||||||
return;
|
&& code.contains("%s")
|
||||||
|
&& (code.contains("gc'")
|
||||||
|
|| code.contains("gc\"")
|
||||||
|
|| code.contains("gc\n")
|
||||||
|
|| code.contains("c'")
|
||||||
|
|| code.contains("c\""))
|
||||||
|
{
|
||||||
|
send_error(id, -32600, "CRITICAL ERROR: The 'c' (confirm) flag in Neovim substitutions is strictly forbidden via MCP as it triggers an interactive prompt that hangs the headless server. Use '/g' or '/ge' instead.").await;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match execute_nvim_lua(code).await {
|
match execute_nvim_lua(code).await {
|
||||||
@@ -1116,8 +1178,3 @@ mod tests {
|
|||||||
assert!(req.is_none());
|
assert!(req.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+2
-1
@@ -12,7 +12,8 @@ fn main() {
|
|||||||
|
|
||||||
let is_dirty = Command::new("git")
|
let is_dirty = Command::new("git")
|
||||||
.args(["status", "--porcelain"])
|
.args(["status", "--porcelain"])
|
||||||
.output().is_ok_and(|out| !out.stdout.is_empty());
|
.output()
|
||||||
|
.is_ok_and(|out| !out.stdout.is_empty());
|
||||||
|
|
||||||
if is_dirty {
|
if is_dirty {
|
||||||
git_hash.push_str("-dirty");
|
git_hash.push_str("-dirty");
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use crate::tools::*;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
pub struct UpdateEnvFingerprintHandler;
|
pub struct UpdateEnvFingerprintHandler;
|
||||||
|
|
||||||
@@ -32,10 +31,7 @@ impl McpTool for UpdateEnvFingerprintHandler {
|
|||||||
os: std::env::consts::OS.to_string(),
|
os: std::env::consts::OS.to_string(),
|
||||||
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
||||||
tool_versions: req.tool_versions,
|
tool_versions: req.tool_versions,
|
||||||
updated_at: SystemTime::now()
|
updated_at: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -61,9 +57,9 @@ impl McpTool for ReadEnvFingerprintHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: ReadEnvFingerprintTool =
|
let req: ReadEnvFingerprintTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.env_fingerprints.read_with(|fps| {
|
let data = state
|
||||||
fps.get(&req.namespace).cloned()
|
.env_fingerprints
|
||||||
});
|
.read_with(|fps| fps.get(&req.namespace).cloned());
|
||||||
if let Some(fp) = data {
|
if let Some(fp) = data {
|
||||||
let data = serde_json::to_string(&fp).unwrap_or_default();
|
let data = serde_json::to_string(&fp).unwrap_or_default();
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -129,10 +125,7 @@ impl McpTool for RegisterEnvironmentHandler {
|
|||||||
url: req.url,
|
url: req.url,
|
||||||
description: req.description,
|
description: req.description,
|
||||||
requires_vpn: req.requires_vpn,
|
requires_vpn: req.requires_vpn,
|
||||||
updated_at: SystemTime::now()
|
updated_at: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Ok("Environment registered".to_string())
|
Ok("Environment registered".to_string())
|
||||||
@@ -158,12 +151,12 @@ impl McpTool for GetEnvironmentDetailsHandler {
|
|||||||
let req: GetEnvironmentDetailsTool =
|
let req: GetEnvironmentDetailsTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.environments.read_with(|envs| {
|
let data = state.environments.read_with(|envs| {
|
||||||
let filtered: Vec<_> = envs.iter().filter(|e| e.namespace == req.namespace).collect();
|
let filtered: Vec<_> = envs
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.namespace == req.namespace)
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6,14 +6,12 @@ use serde_json::Value;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize, Default)]
|
||||||
#[derive(Default)]
|
|
||||||
struct BorrowedGraph<'a> {
|
struct BorrowedGraph<'a> {
|
||||||
entities: std::collections::HashMap<&'a String, &'a crate::models::Entity>,
|
entities: std::collections::HashMap<&'a String, &'a crate::models::Entity>,
|
||||||
relations: Vec<&'a crate::models::Relation>,
|
relations: Vec<&'a crate::models::Relation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct QueryGraphPathHandler;
|
pub struct QueryGraphPathHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -31,13 +29,13 @@ impl McpTool for QueryGraphPathHandler {
|
|||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
state.read_graph(|graph| {
|
state.read_graph(|graph| {
|
||||||
let max_depth = req.max_depth.unwrap_or(5);
|
let max_depth = req.max_depth.unwrap_or(5);
|
||||||
let mut queue = std::collections::VecDeque::new();
|
let mut queue: std::collections::VecDeque<&str> = std::collections::VecDeque::new();
|
||||||
let mut visited = std::collections::HashSet::new();
|
let mut visited: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||||
let mut parents: std::collections::HashMap<String, (String, String)> =
|
let mut parents: std::collections::HashMap<&str, (&str, std::borrow::Cow<'_, str>)> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
queue.push_back(req.start_node.clone());
|
queue.push_back(req.start_node.as_str());
|
||||||
visited.insert(req.start_node.clone());
|
visited.insert(req.start_node.as_str());
|
||||||
|
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
let mut current_depth = 0;
|
let mut current_depth = 0;
|
||||||
@@ -52,21 +50,30 @@ impl McpTool for QueryGraphPathHandler {
|
|||||||
nodes_at_current_depth -= 1;
|
nodes_at_current_depth -= 1;
|
||||||
if current_depth < max_depth {
|
if current_depth < max_depth {
|
||||||
for rel in &graph.relations {
|
for rel in &graph.relations {
|
||||||
if rel.from == current && !visited.contains(&rel.to) {
|
if rel.from == current && !visited.contains(rel.to.as_str()) {
|
||||||
visited.insert(rel.to.clone());
|
visited.insert(rel.to.as_str());
|
||||||
parents.insert(
|
parents.insert(
|
||||||
rel.to.clone(),
|
rel.to.as_str(),
|
||||||
(current.clone(), rel.relation_type.clone()),
|
(
|
||||||
|
current,
|
||||||
|
std::borrow::Cow::Borrowed(rel.relation_type.as_str()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
queue.push_back(rel.to.clone());
|
queue.push_back(rel.to.as_str());
|
||||||
nodes_at_next_depth += 1;
|
nodes_at_next_depth += 1;
|
||||||
} else if rel.to == current && !visited.contains(&rel.from) {
|
} else if rel.to == current && !visited.contains(rel.from.as_str()) {
|
||||||
visited.insert(rel.from.clone());
|
visited.insert(rel.from.as_str());
|
||||||
parents.insert(
|
parents.insert(
|
||||||
rel.from.clone(),
|
rel.from.as_str(),
|
||||||
(current.clone(), format!("inverse({})", rel.relation_type)),
|
(
|
||||||
|
current,
|
||||||
|
std::borrow::Cow::Owned(format!(
|
||||||
|
"inverse({})",
|
||||||
|
rel.relation_type
|
||||||
|
)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
queue.push_back(rel.from.clone());
|
queue.push_back(rel.from.as_str());
|
||||||
nodes_at_next_depth += 1;
|
nodes_at_next_depth += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,11 +87,11 @@ impl McpTool for QueryGraphPathHandler {
|
|||||||
|
|
||||||
if found {
|
if found {
|
||||||
let mut path = Vec::new();
|
let mut path = Vec::new();
|
||||||
let mut curr = req.end_node.clone();
|
let mut curr = req.end_node.as_str();
|
||||||
while curr != req.start_node {
|
while curr != req.start_node {
|
||||||
if let Some((parent, rel_type)) = parents.get(&curr) {
|
if let Some((parent, rel_type)) = parents.get(&curr) {
|
||||||
path.push(format!("{} -[{}]-> {}", parent, rel_type, curr));
|
path.push(format!("{} -[{}]-> {}", parent, rel_type, curr));
|
||||||
curr = parent.clone();
|
curr = parent;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -124,9 +131,13 @@ impl McpTool for CreateEntitiesHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone();
|
let idx = state
|
||||||
|
.search_index
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.clone();
|
||||||
for entity in inserted {
|
for entity in inserted {
|
||||||
let _ = idx.index_entity(&entity);
|
drop(idx.index_entity(&entity));
|
||||||
}
|
}
|
||||||
Ok("Entities created".to_string())
|
Ok("Entities created".to_string())
|
||||||
}
|
}
|
||||||
@@ -206,9 +217,13 @@ impl McpTool for DeleteEntitiesHandler {
|
|||||||
.retain(|r| !to_delete.contains(&r.from) && !to_delete.contains(&r.to));
|
.retain(|r| !to_delete.contains(&r.from) && !to_delete.contains(&r.to));
|
||||||
});
|
});
|
||||||
|
|
||||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone();
|
let idx = state
|
||||||
|
.search_index
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.clone();
|
||||||
for name in to_delete {
|
for name in to_delete {
|
||||||
let _ = idx.delete_document(&name);
|
drop(idx.delete_document(&name));
|
||||||
}
|
}
|
||||||
Ok("Entities deleted".to_string())
|
Ok("Entities deleted".to_string())
|
||||||
}
|
}
|
||||||
@@ -416,7 +431,10 @@ impl McpTool for VisualizeGraphHandler {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if query.is_empty() || included.contains(r.from.as_str()) || included.contains(r.to.as_str()) {
|
if query.is_empty()
|
||||||
|
|| included.contains(r.from.as_str())
|
||||||
|
|| included.contains(r.to.as_str())
|
||||||
|
{
|
||||||
included.insert(r.from.as_str());
|
included.insert(r.from.as_str());
|
||||||
included.insert(r.to.as_str());
|
included.insert(r.to.as_str());
|
||||||
to_draw.push(r);
|
to_draw.push(r);
|
||||||
|
|||||||
+115
-71
@@ -5,7 +5,6 @@ use crate::tools::*;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
pub struct LogDecisionHandler;
|
pub struct LogDecisionHandler;
|
||||||
|
|
||||||
@@ -22,7 +21,11 @@ impl McpTool for LogDecisionHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: LogDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: LogDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone();
|
let idx = state
|
||||||
|
.search_index
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.clone();
|
||||||
let mut final_id = String::new();
|
let mut final_id = String::new();
|
||||||
|
|
||||||
state.adrs.modify(|adrs| {
|
state.adrs.modify(|adrs| {
|
||||||
@@ -33,13 +36,10 @@ impl McpTool for LogDecisionHandler {
|
|||||||
context: req.context,
|
context: req.context,
|
||||||
decision: req.decision,
|
decision: req.decision,
|
||||||
consequence: req.consequence,
|
consequence: req.consequence,
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = idx.index_adr(&a);
|
drop(idx.index_adr(&a));
|
||||||
adrs.push(a);
|
adrs.push(a);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,15 +62,18 @@ impl McpTool for QueryDecisionsHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: QueryDecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: QueryDecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.adrs.read_with(|adrs| {
|
let data = state.adrs.read_with(|adrs| {
|
||||||
let filtered: Vec<_> = adrs.iter().filter(|a| {
|
let filtered: Vec<_> = adrs
|
||||||
if let Some(q) = &req.query {
|
.iter()
|
||||||
contains_ignore_ascii_case(&a.title, q)
|
.filter(|a| {
|
||||||
|| contains_ignore_ascii_case(&a.context, q)
|
if let Some(q) = &req.query {
|
||||||
|| contains_ignore_ascii_case(&a.decision, q)
|
contains_ignore_ascii_case(&a.title, q)
|
||||||
} else {
|
|| contains_ignore_ascii_case(&a.context, q)
|
||||||
true
|
|| contains_ignore_ascii_case(&a.decision, q)
|
||||||
}
|
} else {
|
||||||
}).collect();
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -95,10 +98,7 @@ impl McpTool for LogErrorFixHandler {
|
|||||||
fixes.push(crate::models::ErrorFix {
|
fixes.push(crate::models::ErrorFix {
|
||||||
signature: req.signature,
|
signature: req.signature,
|
||||||
solution: req.solution,
|
solution: req.solution,
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
})
|
})
|
||||||
@@ -126,10 +126,13 @@ impl McpTool for SearchErrorFixesHandler {
|
|||||||
let req: SearchErrorFixesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: SearchErrorFixesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let q = req.query;
|
let q = req.query;
|
||||||
let data = state.error_fixes.read_with(|fixes| {
|
let data = state.error_fixes.read_with(|fixes| {
|
||||||
let filtered: Vec<_> = fixes.iter().filter(|f| {
|
let filtered: Vec<_> = fixes
|
||||||
contains_ignore_ascii_case(&f.signature, &q)
|
.iter()
|
||||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
.filter(|f| {
|
||||||
}).collect();
|
contains_ignore_ascii_case(&f.signature, &q)
|
||||||
|
|| contains_ignore_ascii_case(&f.solution, &q)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -152,10 +155,7 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
state.ledger.modify(|ledger| {
|
state.ledger.modify(|ledger| {
|
||||||
ledger.push(CodeChange {
|
ledger.push(CodeChange {
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
file_path: req.file_path,
|
file_path: req.file_path,
|
||||||
description: req.description,
|
description: req.description,
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
@@ -182,7 +182,9 @@ impl McpTool for QueryRecentChangesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let data = state.ledger.read_with(|l| serde_json::to_string(l).unwrap_or_else(|_| "[]".to_string()));
|
let data = state
|
||||||
|
.ledger
|
||||||
|
.read_with(|l| serde_json::to_string(l).unwrap_or_else(|_| "[]".to_string()));
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,10 +209,7 @@ impl McpTool for LearnPreferenceHandler {
|
|||||||
crate::models::Preference {
|
crate::models::Preference {
|
||||||
key: req.key.clone(),
|
key: req.key.clone(),
|
||||||
value: req.value,
|
value: req.value,
|
||||||
updated_at: SystemTime::now()
|
updated_at: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -231,7 +230,9 @@ impl McpTool for ReadPreferencesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let data = state.prefs.read_with(|prefs| serde_json::to_string(prefs).unwrap_or_default());
|
let data = state
|
||||||
|
.prefs
|
||||||
|
.read_with(|prefs| serde_json::to_string(prefs).unwrap_or_default());
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,10 +258,7 @@ impl McpTool for LogTechDebtHandler {
|
|||||||
description: req.description,
|
description: req.description,
|
||||||
ideal_solution: req.ideal_solution,
|
ideal_solution: req.ideal_solution,
|
||||||
is_resolved: false,
|
is_resolved: false,
|
||||||
created_at: SystemTime::now()
|
created_at: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
})
|
})
|
||||||
@@ -319,9 +317,12 @@ impl McpTool for ListTechDebtHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: ListTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ListTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.tech_debts.read_with(|debts| {
|
let data = state.tech_debts.read_with(|debts| {
|
||||||
let filtered: Vec<_> = debts.iter().filter(|d| {
|
let filtered: Vec<_> = debts
|
||||||
d.namespace == req.namespace && (req.include_resolved || !d.is_resolved)
|
.iter()
|
||||||
}).collect();
|
.filter(|d| {
|
||||||
|
d.namespace == req.namespace && (req.include_resolved || !d.is_resolved)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -361,49 +362,73 @@ impl McpTool for OmniSearchHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let tasks_json = state.tasks.read_with(|all_tasks| {
|
let tasks_json = state.tasks.read_with(|all_tasks| {
|
||||||
let filtered: Vec<_> = all_tasks.iter().filter(|t| {
|
let filtered: Vec<_> = all_tasks
|
||||||
matches.iter().any(|(id, typ, _, _, _)| id == &t.id && typ == "task")
|
.iter()
|
||||||
}).collect();
|
.filter(|t| {
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.any(|(id, typ, _, _, _)| id == &t.id && typ == "task")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_value(&filtered).unwrap_or_default()
|
serde_json::to_value(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let snippets_json = state.snippets.read_with(|all_snippets| {
|
let snippets_json = state.snippets.read_with(|all_snippets| {
|
||||||
let filtered: Vec<_> = all_snippets.iter().filter(|s| {
|
let filtered: Vec<_> = all_snippets
|
||||||
matches.iter().any(|(id, typ, _, _, _)| id == &s.name && typ == "snippet")
|
.iter()
|
||||||
}).collect();
|
.filter(|s| {
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.any(|(id, typ, _, _, _)| id == &s.name && typ == "snippet")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_value(&filtered).unwrap_or_default()
|
serde_json::to_value(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let adrs_json = state.adrs.read_with(|all_adrs| {
|
let adrs_json = state.adrs.read_with(|all_adrs| {
|
||||||
let filtered: Vec<_> = all_adrs.iter().filter(|a| {
|
let filtered: Vec<_> = all_adrs
|
||||||
matches.iter().any(|(id, typ, _, _, _)| id == &a.id && typ == "adr")
|
.iter()
|
||||||
}).collect();
|
.filter(|a| {
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.any(|(id, typ, _, _, _)| id == &a.id && typ == "adr")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_value(&filtered).unwrap_or_default()
|
serde_json::to_value(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let q = req.query;
|
let q = req.query;
|
||||||
let tech_debts_json = state.tech_debts.read_with(|debts| {
|
let tech_debts_json = state.tech_debts.read_with(|debts| {
|
||||||
let filtered: Vec<_> = debts.iter().filter(|d| {
|
let filtered: Vec<_> = debts
|
||||||
req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns)
|
.iter()
|
||||||
&& (contains_ignore_ascii_case(&d.description, &q)
|
.filter(|d| {
|
||||||
|| contains_ignore_ascii_case(&d.ideal_solution, &q))
|
req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns)
|
||||||
}).collect();
|
&& (contains_ignore_ascii_case(&d.description, &q)
|
||||||
|
|| contains_ignore_ascii_case(&d.ideal_solution, &q))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_value(&filtered).unwrap_or_default()
|
serde_json::to_value(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let memos_json = state.handoff_memos.read_with(|memos| {
|
let memos_json = state.handoff_memos.read_with(|memos| {
|
||||||
let filtered: Vec<_> = memos.iter().filter(|m| {
|
let filtered: Vec<_> = memos
|
||||||
req.namespace.as_ref().is_none_or(|ns| m.namespace == *ns)
|
.iter()
|
||||||
&& contains_ignore_ascii_case(&m.content, &q)
|
.filter(|m| {
|
||||||
}).collect();
|
req.namespace.as_ref().is_none_or(|ns| m.namespace == *ns)
|
||||||
|
&& contains_ignore_ascii_case(&m.content, &q)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_value(&filtered).unwrap_or_default()
|
serde_json::to_value(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let error_fixes_json = state.error_fixes.read_with(|fixes| {
|
let error_fixes_json = state.error_fixes.read_with(|fixes| {
|
||||||
let filtered: Vec<_> = fixes.iter().filter(|f| {
|
let filtered: Vec<_> = fixes
|
||||||
contains_ignore_ascii_case(&f.signature, &q)
|
.iter()
|
||||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
.filter(|f| {
|
||||||
}).collect();
|
contains_ignore_ascii_case(&f.signature, &q)
|
||||||
|
|| contains_ignore_ascii_case(&f.solution, &q)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_value(&filtered).unwrap_or_default()
|
serde_json::to_value(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -437,11 +462,33 @@ impl McpTool for GetProjectHealthHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let active_tasks = state.tasks.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count());
|
let active_tasks = state
|
||||||
let unresolved_debt = state.tech_debts.read_with(|debts| debts.iter().filter(|d| d.namespace == req.namespace && !d.is_resolved).count());
|
.tasks
|
||||||
let unread_memos = state.handoff_memos.read_with(|memos| memos.iter().filter(|m| m.namespace == req.namespace).count());
|
.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count());
|
||||||
let active_milestones = state.milestones.read_with(|milestones| milestones.iter().filter(|m| m.namespace == req.namespace && m.status != "done").count());
|
let unresolved_debt = state.tech_debts.read_with(|debts| {
|
||||||
let remaining_checklists = state.pr_checklists.read_with(|checklists| checklists.iter().filter(|c| c.namespace == req.namespace).count());
|
debts
|
||||||
|
.iter()
|
||||||
|
.filter(|d| d.namespace == req.namespace && !d.is_resolved)
|
||||||
|
.count()
|
||||||
|
});
|
||||||
|
let unread_memos = state.handoff_memos.read_with(|memos| {
|
||||||
|
memos
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.namespace == req.namespace)
|
||||||
|
.count()
|
||||||
|
});
|
||||||
|
let active_milestones = state.milestones.read_with(|milestones| {
|
||||||
|
milestones
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.namespace == req.namespace && m.status != "done")
|
||||||
|
.count()
|
||||||
|
});
|
||||||
|
let remaining_checklists = state.pr_checklists.read_with(|checklists| {
|
||||||
|
checklists
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.namespace == req.namespace)
|
||||||
|
.count()
|
||||||
|
});
|
||||||
|
|
||||||
let report = serde_json::json!({
|
let report = serde_json::json!({
|
||||||
"active_tasks": active_tasks,
|
"active_tasks": active_tasks,
|
||||||
@@ -455,6 +502,3 @@ impl McpTool for GetProjectHealthHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use crate::handlers_v2::utils::*;
|
use crate::handlers_v2::utils::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6,7 +6,6 @@ use async_trait::async_trait;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
pub struct AddStickyNoteHandler;
|
pub struct AddStickyNoteHandler;
|
||||||
|
|
||||||
@@ -24,10 +23,7 @@ impl McpTool for AddStickyNoteHandler {
|
|||||||
let req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
state.sticky.modify(|notes| {
|
state.sticky.modify(|notes| {
|
||||||
notes.push(StickyNote {
|
notes.push(StickyNote {
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
content: req.content,
|
content: req.content,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -51,7 +47,9 @@ impl McpTool for ReadStickyNotesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let data = state.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
let data = state
|
||||||
|
.sticky
|
||||||
|
.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,10 +132,7 @@ impl McpTool for LeaveHandoffMemoHandler {
|
|||||||
author: "agy".to_string(),
|
author: "agy".to_string(),
|
||||||
content: req.content,
|
content: req.content,
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Handoff memo left".to_string())
|
Ok("Handoff memo left".to_string())
|
||||||
@@ -162,13 +157,16 @@ impl McpTool for ReadHandoffMemosHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.handoff_memos.read_with(|items| {
|
let data = state.handoff_memos.read_with(|items| {
|
||||||
let filtered: Vec<_> = items.iter().filter(|i| {
|
let filtered: Vec<_> = items
|
||||||
if let Some(ns) = &req.namespace {
|
.iter()
|
||||||
&i.namespace == ns
|
.filter(|i| {
|
||||||
} else {
|
if let Some(ns) = &req.namespace {
|
||||||
true
|
&i.namespace == ns
|
||||||
}
|
} else {
|
||||||
}).collect();
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -221,10 +219,7 @@ impl McpTool for AddSessionSummaryHandler {
|
|||||||
summaries.push(crate::models::SessionSummary {
|
summaries.push(crate::models::SessionSummary {
|
||||||
summary: req.summary,
|
summary: req.summary,
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Session summary added".to_string())
|
Ok("Session summary added".to_string())
|
||||||
@@ -249,11 +244,8 @@ impl McpTool for GenerateStandupReportHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: GenerateStandupReportTool =
|
let req: GenerateStandupReportTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let cutoff = SystemTime::now()
|
let cutoff =
|
||||||
.duration_since(UNIX_EPOCH)
|
crate::handlers_v2::utils::now_secs().saturating_sub(req.hours_lookback * 3600);
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs()
|
|
||||||
.saturating_sub(req.hours_lookback * 3600);
|
|
||||||
|
|
||||||
let report_str = state.tasks.read_with(|items| {
|
let report_str = state.tasks.read_with(|items| {
|
||||||
state.ledger.read_with(|changes| {
|
state.ledger.read_with(|changes| {
|
||||||
@@ -269,4 +261,3 @@ impl McpTool for GenerateStandupReportHandler {
|
|||||||
Ok(report_str)
|
Ok(report_str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5,7 +5,6 @@ use crate::tools::*;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
pub struct AddTaskHandler;
|
pub struct AddTaskHandler;
|
||||||
|
|
||||||
@@ -21,10 +20,7 @@ impl McpTool for AddTaskHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let now = SystemTime::now()
|
let now = crate::handlers_v2::utils::now_secs();
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
let task_id = uuid::Uuid::new_v4().to_string();
|
let task_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
let deps = req.dependencies.unwrap_or_default();
|
let deps = req.dependencies.unwrap_or_default();
|
||||||
@@ -41,8 +37,12 @@ impl McpTool for AddTaskHandler {
|
|||||||
dependencies: deps,
|
dependencies: deps,
|
||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
};
|
};
|
||||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone();
|
let idx = state
|
||||||
let _ = idx.index_task(&task);
|
.search_index
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.clone();
|
||||||
|
drop(idx.index_task(&task));
|
||||||
state.tasks.modify(|tasks| {
|
state.tasks.modify(|tasks| {
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
});
|
});
|
||||||
@@ -70,7 +70,8 @@ impl McpTool for DeleteTaskHandler {
|
|||||||
let initial_len = tasks.len();
|
let initial_len = tasks.len();
|
||||||
|
|
||||||
// Build index-based children map
|
// Build index-based children map
|
||||||
let mut children_map: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
|
let mut children_map: std::collections::HashMap<usize, Vec<usize>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
let mut id_to_index = std::collections::HashMap::new();
|
let mut id_to_index = std::collections::HashMap::new();
|
||||||
for (idx, t) in tasks.iter().enumerate() {
|
for (idx, t) in tasks.iter().enumerate() {
|
||||||
id_to_index.insert(t.id.as_str(), idx);
|
id_to_index.insert(t.id.as_str(), idx);
|
||||||
@@ -78,9 +79,10 @@ impl McpTool for DeleteTaskHandler {
|
|||||||
|
|
||||||
for (idx, t) in tasks.iter().enumerate() {
|
for (idx, t) in tasks.iter().enumerate() {
|
||||||
if let Some(pid) = &t.parent_id
|
if let Some(pid) = &t.parent_id
|
||||||
&& let Some(&parent_idx) = id_to_index.get(pid.as_str()) {
|
&& let Some(&parent_idx) = id_to_index.get(pid.as_str())
|
||||||
children_map.entry(parent_idx).or_default().push(idx);
|
{
|
||||||
}
|
children_map.entry(parent_idx).or_default().push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut to_delete_idx = std::collections::HashSet::new();
|
let mut to_delete_idx = std::collections::HashSet::new();
|
||||||
@@ -90,9 +92,10 @@ impl McpTool for DeleteTaskHandler {
|
|||||||
|
|
||||||
while let Some(curr) = queue.pop_front() {
|
while let Some(curr) = queue.pop_front() {
|
||||||
if to_delete_idx.insert(curr)
|
if to_delete_idx.insert(curr)
|
||||||
&& let Some(children) = children_map.get(&curr) {
|
&& let Some(children) = children_map.get(&curr)
|
||||||
queue.extend(children.iter().copied());
|
{
|
||||||
}
|
queue.extend(children.iter().copied());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,9 +108,13 @@ impl McpTool for DeleteTaskHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if deleted_count > 0 {
|
if deleted_count > 0 {
|
||||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone();
|
let idx = state
|
||||||
|
.search_index
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.clone();
|
||||||
for id in actually_deleted {
|
for id in actually_deleted {
|
||||||
let _ = idx.delete_document(&id);
|
drop(idx.delete_document(&id));
|
||||||
}
|
}
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
format!("Deleted task and its children ({} total).", deleted_count).to_string(),
|
format!("Deleted task and its children ({} total).", deleted_count).to_string(),
|
||||||
@@ -143,7 +150,9 @@ impl McpTool for UpdateTaskStatusHandler {
|
|||||||
|
|
||||||
state.tasks.modify(|tasks| {
|
state.tasks.modify(|tasks| {
|
||||||
// Find target task
|
// Find target task
|
||||||
let target_idx = tasks.iter().position(|t| t.id == req.id || t.title == req.id);
|
let target_idx = tasks
|
||||||
|
.iter()
|
||||||
|
.position(|t| t.id == req.id || t.title == req.id);
|
||||||
let target_idx = match target_idx {
|
let target_idx = match target_idx {
|
||||||
Some(idx) => idx,
|
Some(idx) => idx,
|
||||||
None => return,
|
None => return,
|
||||||
@@ -154,7 +163,11 @@ impl McpTool for UpdateTaskStatusHandler {
|
|||||||
|
|
||||||
if target_status == "done" || target_status == "completed" {
|
if target_status == "done" || target_status == "completed" {
|
||||||
// 1. Check Acceptance Criteria
|
// 1. Check Acceptance Criteria
|
||||||
if tasks[target_idx].acceptance_criteria.iter().any(|c| !c.is_met) {
|
if tasks[target_idx]
|
||||||
|
.acceptance_criteria
|
||||||
|
.iter()
|
||||||
|
.any(|c| !c.is_met)
|
||||||
|
{
|
||||||
blocked = true;
|
blocked = true;
|
||||||
blocker_details = "Unmet acceptance criteria exist.".to_string();
|
blocker_details = "Unmet acceptance criteria exist.".to_string();
|
||||||
}
|
}
|
||||||
@@ -164,27 +177,36 @@ impl McpTool for UpdateTaskStatusHandler {
|
|||||||
let mut uncompleted_deps = Vec::new();
|
let mut uncompleted_deps = Vec::new();
|
||||||
for dep_id in &tasks[target_idx].dependencies {
|
for dep_id in &tasks[target_idx].dependencies {
|
||||||
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
|
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
|
||||||
&& dep_task.status != "completed" && dep_task.status != "done" {
|
&& dep_task.status != "completed"
|
||||||
uncompleted_deps.push(dep_task.title.as_str());
|
&& dep_task.status != "done"
|
||||||
}
|
{
|
||||||
|
uncompleted_deps.push(dep_task.title.as_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !uncompleted_deps.is_empty() {
|
if !uncompleted_deps.is_empty() {
|
||||||
blocked = true;
|
blocked = true;
|
||||||
blocker_details = format!("Blocked by dependencies: {}", uncompleted_deps.join(", "));
|
blocker_details =
|
||||||
|
format!("Blocked by dependencies: {}", uncompleted_deps.join(", "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check child tasks
|
// 3. Check child tasks
|
||||||
if !blocked {
|
if !blocked {
|
||||||
let mut uncompleted_children = Vec::new();
|
let mut uncompleted_children = Vec::new();
|
||||||
for child in tasks.iter().filter(|t| t.parent_id.as_ref() == Some(&target_id)) {
|
for child in tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.parent_id.as_ref() == Some(&target_id))
|
||||||
|
{
|
||||||
if child.status != "completed" && child.status != "done" {
|
if child.status != "completed" && child.status != "done" {
|
||||||
uncompleted_children.push(child.title.as_str());
|
uncompleted_children.push(child.title.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !uncompleted_children.is_empty() {
|
if !uncompleted_children.is_empty() {
|
||||||
blocked = true;
|
blocked = true;
|
||||||
blocker_details = format!("Blocked by child tasks: {}", uncompleted_children.join(", "));
|
blocker_details = format!(
|
||||||
|
"Blocked by child tasks: {}",
|
||||||
|
uncompleted_children.join(", ")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,10 +214,7 @@ impl McpTool for UpdateTaskStatusHandler {
|
|||||||
if !blocked {
|
if !blocked {
|
||||||
// Apply update
|
// Apply update
|
||||||
tasks[target_idx].status = target_status.clone();
|
tasks[target_idx].status = target_status.clone();
|
||||||
tasks[target_idx].updated_at = SystemTime::now()
|
tasks[target_idx].updated_at = crate::handlers_v2::utils::now_secs();
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
// Cascade cancellation to children
|
// Cascade cancellation to children
|
||||||
if target_status == "cancelled" || target_status == "abandoned" {
|
if target_status == "cancelled" || target_status == "abandoned" {
|
||||||
@@ -210,9 +229,10 @@ impl McpTool for UpdateTaskStatusHandler {
|
|||||||
|
|
||||||
for (idx, t) in tasks.iter().enumerate() {
|
for (idx, t) in tasks.iter().enumerate() {
|
||||||
if let Some(pid) = &t.parent_id
|
if let Some(pid) = &t.parent_id
|
||||||
&& let Some(&p_idx) = id_to_idx.get(pid.as_str()) {
|
&& let Some(&p_idx) = id_to_idx.get(pid.as_str())
|
||||||
children_map.entry(p_idx).or_default().push(idx);
|
{
|
||||||
}
|
children_map.entry(p_idx).or_default().push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(&start_idx) = id_to_idx.get(target_id.as_str()) {
|
if let Some(&start_idx) = id_to_idx.get(target_id.as_str()) {
|
||||||
@@ -267,14 +287,20 @@ impl McpTool for ListActiveTasksHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: ListActiveTasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ListActiveTasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.tasks.read_with(|tasks| {
|
let data = state.tasks.read_with(|tasks| {
|
||||||
let filtered: Vec<_> = tasks.iter().filter(|t| {
|
let filtered: Vec<_> = tasks
|
||||||
let status_match = t.status != "done";
|
.iter()
|
||||||
let branch_match = match &req.git_branch {
|
.filter(|t| {
|
||||||
Some(branch) => t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()),
|
let status_match = t.status != "done";
|
||||||
None => true,
|
let branch_match = match &req.git_branch {
|
||||||
};
|
Some(branch) => {
|
||||||
status_match && branch_match
|
t.git_branch.is_none()
|
||||||
}).collect();
|
|| t.git_branch.as_deref() == Some(branch.as_str())
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
status_match && branch_match
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data)
|
Ok(data)
|
||||||
@@ -311,10 +337,7 @@ impl McpTool for SetAcceptanceCriteriaHandler {
|
|||||||
is_met: false,
|
is_met: false,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
task.updated_at = SystemTime::now()
|
task.updated_at = crate::handlers_v2::utils::now_secs();
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -358,10 +381,7 @@ impl McpTool for VerifyAcceptanceCriteriaHandler {
|
|||||||
} else {
|
} else {
|
||||||
ac.is_met = true;
|
ac.is_met = true;
|
||||||
success = true;
|
success = true;
|
||||||
task.updated_at = SystemTime::now()
|
task.updated_at = crate::handlers_v2::utils::now_secs();
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -452,13 +472,16 @@ impl McpTool for ListMilestonesHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: ListMilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ListMilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.milestones.read_with(|items| {
|
let data = state.milestones.read_with(|items| {
|
||||||
let filtered: Vec<_> = items.iter().filter(|i| {
|
let filtered: Vec<_> = items
|
||||||
if let Some(ns) = &req.namespace {
|
.iter()
|
||||||
&i.namespace == ns
|
.filter(|i| {
|
||||||
} else {
|
if let Some(ns) = &req.namespace {
|
||||||
true
|
&i.namespace == ns
|
||||||
}
|
} else {
|
||||||
}).collect();
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data)
|
Ok(data)
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ pub fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
|
|||||||
.windows(needle.len())
|
.windows(needle.len())
|
||||||
.any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
|
.any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
|
||||||
}
|
}
|
||||||
|
pub fn now_secs() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ use crate::tools::*;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
pub struct PinFileHandler;
|
pub struct PinFileHandler;
|
||||||
|
|
||||||
@@ -26,10 +25,7 @@ impl McpTool for PinFileHandler {
|
|||||||
pinned.push(crate::models::PinnedFile {
|
pinned.push(crate::models::PinnedFile {
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
file_path: req.file_path,
|
file_path: req.file_path,
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -76,17 +72,23 @@ impl McpTool for ListPinnedFilesHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: ListPinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ListPinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.pinned_files.read_with(|pinned| {
|
let data = state.pinned_files.read_with(|pinned| {
|
||||||
let filtered: Vec<_> = pinned.iter().filter(|p| {
|
let filtered: Vec<_> = pinned
|
||||||
let ns_match = match &req.namespace {
|
.iter()
|
||||||
Some(ns) => &p.namespace == ns,
|
.filter(|p| {
|
||||||
std::option::Option::None => true,
|
let ns_match = match &req.namespace {
|
||||||
};
|
Some(ns) => &p.namespace == ns,
|
||||||
let branch_match = match &req.git_branch {
|
std::option::Option::None => true,
|
||||||
Some(branch) => p.git_branch.is_none() || p.git_branch.as_deref() == Some(branch.as_str()),
|
};
|
||||||
std::option::Option::None => true,
|
let branch_match = match &req.git_branch {
|
||||||
};
|
Some(branch) => {
|
||||||
ns_match && branch_match
|
p.git_branch.is_none()
|
||||||
}).collect();
|
|| p.git_branch.as_deref() == Some(branch.as_str())
|
||||||
|
}
|
||||||
|
std::option::Option::None => true,
|
||||||
|
};
|
||||||
|
ns_match && branch_match
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -113,14 +115,15 @@ impl McpTool for StoreSnippetHandler {
|
|||||||
language: req.language,
|
language: req.language,
|
||||||
code: req.code,
|
code: req.code,
|
||||||
description: req.description,
|
description: req.description,
|
||||||
updated_at: SystemTime::now()
|
updated_at: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone();
|
let idx = state
|
||||||
let _ = idx.index_snippet(&snippet);
|
.search_index
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.clone();
|
||||||
|
drop(idx.index_snippet(&snippet));
|
||||||
|
|
||||||
state.snippets.modify(|snippets| {
|
state.snippets.modify(|snippets| {
|
||||||
snippets.retain(|s| s.name != req_name);
|
snippets.retain(|s| s.name != req_name);
|
||||||
@@ -147,11 +150,14 @@ impl McpTool for SearchSnippetsHandler {
|
|||||||
let req: SearchSnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: SearchSnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let query = req.query;
|
let query = req.query;
|
||||||
let data = state.snippets.read_with(|snippets| {
|
let data = state.snippets.read_with(|snippets| {
|
||||||
let results: Vec<_> = snippets.iter().filter(|s| {
|
let results: Vec<_> = snippets
|
||||||
contains_ignore_ascii_case(&s.name, &query)
|
.iter()
|
||||||
|| contains_ignore_ascii_case(&s.description, &query)
|
.filter(|s| {
|
||||||
|| contains_ignore_ascii_case(&s.language, &query)
|
contains_ignore_ascii_case(&s.name, &query)
|
||||||
}).collect();
|
|| contains_ignore_ascii_case(&s.description, &query)
|
||||||
|
|| contains_ignore_ascii_case(&s.language, &query)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&results).unwrap_or_default()
|
serde_json::to_string(&results).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -179,8 +185,12 @@ impl McpTool for DeleteSnippetHandler {
|
|||||||
deleted = snippets.len() < orig;
|
deleted = snippets.len() < orig;
|
||||||
});
|
});
|
||||||
if deleted {
|
if deleted {
|
||||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone();
|
let idx = state
|
||||||
let _ = idx.delete_document(&req.name);
|
.search_index
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.clone();
|
||||||
|
drop(idx.delete_document(&req.name));
|
||||||
Ok("Snippet deleted.".to_string())
|
Ok("Snippet deleted.".to_string())
|
||||||
} else {
|
} else {
|
||||||
Ok("Snippet not found.".to_string())
|
Ok("Snippet not found.".to_string())
|
||||||
@@ -213,10 +223,7 @@ impl McpTool for SaveContextWorkspaceHandler {
|
|||||||
name: req.name,
|
name: req.name,
|
||||||
pinned_files: req.pinned_files,
|
pinned_files: req.pinned_files,
|
||||||
active_task_ids: req.active_task_ids,
|
active_task_ids: req.active_task_ids,
|
||||||
saved_at: SystemTime::now()
|
saved_at: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Ok("Context workspace saved".to_string())
|
Ok("Context workspace saved".to_string())
|
||||||
@@ -242,9 +249,10 @@ impl McpTool for LoadContextWorkspaceHandler {
|
|||||||
let req: LoadContextWorkspaceTool =
|
let req: LoadContextWorkspaceTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.context_workspaces.read_with(|ws| {
|
let data = state.context_workspaces.read_with(|ws| {
|
||||||
let filtered: Vec<_> = ws.iter().filter(|w| {
|
let filtered: Vec<_> = ws
|
||||||
w.namespace == req.namespace && w.name == req.name
|
.iter()
|
||||||
}).collect();
|
.filter(|w| w.namespace == req.namespace && w.name == req.name)
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered.first()).unwrap_or_default()
|
serde_json::to_string(&filtered.first()).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -270,9 +278,7 @@ impl McpTool for ListContextWorkspacesHandler {
|
|||||||
let req: ListContextWorkspacesTool =
|
let req: ListContextWorkspacesTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.context_workspaces.read_with(|ws| {
|
let data = state.context_workspaces.read_with(|ws| {
|
||||||
let filtered: Vec<_> = ws.iter().filter(|w| {
|
let filtered: Vec<_> = ws.iter().filter(|w| w.namespace == req.namespace).collect();
|
||||||
w.namespace == req.namespace
|
|
||||||
}).collect();
|
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -323,9 +329,10 @@ impl McpTool for GetPrChecklistHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: GetPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: GetPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let data = state.pr_checklists.read_with(|items| {
|
let data = state.pr_checklists.read_with(|items| {
|
||||||
let filtered: Vec<_> = items.iter().filter(|i| {
|
let filtered: Vec<_> = items
|
||||||
i.namespace == req.namespace
|
.iter()
|
||||||
}).collect();
|
.filter(|i| i.namespace == req.namespace)
|
||||||
|
.collect();
|
||||||
serde_json::to_string(&filtered).unwrap_or_default()
|
serde_json::to_string(&filtered).unwrap_or_default()
|
||||||
});
|
});
|
||||||
Ok(data.to_string())
|
Ok(data.to_string())
|
||||||
@@ -357,5 +364,3 @@ impl McpTool for ClearPrChecklistHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use crate::handlers_v2::utils::*;
|
use crate::handlers_v2::utils::*;
|
||||||
|
|
||||||
|
|
||||||
+26
-17
@@ -22,7 +22,7 @@ use redb::ReadableTable;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -204,10 +204,7 @@ async fn gate_set_handler(
|
|||||||
params: body.params.clone(),
|
params: body.params.clone(),
|
||||||
status,
|
status,
|
||||||
reason: body.reason.clone(),
|
reason: body.reason.clone(),
|
||||||
timestamp: SystemTime::now()
|
timestamp: crate::handlers_v2::utils::now_secs(),
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs(),
|
|
||||||
};
|
};
|
||||||
app_state.handler.state.gates.modify(|gates| {
|
app_state.handler.state.gates.modify(|gates| {
|
||||||
gates.retain(|g| !(g.action == record.action && g.target == record.target));
|
gates.retain(|g| !(g.action == record.action && g.target == record.target));
|
||||||
@@ -430,7 +427,9 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
|||||||
|
|
||||||
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
||||||
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr).parse().expect("Invalid bind address");
|
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr)
|
||||||
|
.parse()
|
||||||
|
.expect("Invalid bind address");
|
||||||
|
|
||||||
let listener = match tokio::net::TcpListener::bind(&addr).await {
|
let listener = match tokio::net::TcpListener::bind(&addr).await {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
@@ -726,14 +725,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if !cli.daemon {
|
if !cli.daemon {
|
||||||
// Just spawn the daemon and exit. We no longer act as a proxy.
|
// Just spawn the daemon and exit. We no longer act as a proxy.
|
||||||
#[allow(clippy::zombie_processes)]
|
#[allow(clippy::zombie_processes)]
|
||||||
let _ = std::process::Command::new(std::env::current_exe().expect("Failed to get current executable path"))
|
let _ = std::process::Command::new(
|
||||||
.arg("--daemon")
|
std::env::current_exe().expect("Failed to get current executable path"),
|
||||||
.stdin(std::process::Stdio::null())
|
)
|
||||||
.stdout(std::process::Stdio::null())
|
.arg("--daemon")
|
||||||
.stderr(std::process::Stdio::null())
|
.stdin(std::process::Stdio::null())
|
||||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
.stdout(std::process::Stdio::null())
|
||||||
.spawn()
|
.stderr(std::process::Stdio::null())
|
||||||
.expect("Failed to spawn daemon");
|
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to spawn daemon");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -752,7 +753,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
{
|
{
|
||||||
let write_txn = db.begin_write().expect("Failed to begin write txn on redb");
|
let write_txn = db.begin_write().expect("Failed to begin write txn on redb");
|
||||||
{
|
{
|
||||||
let mut table = write_txn.open_table(crate::store::STORE_TABLE).expect("Failed to open STORE_TABLE");
|
let mut table = write_txn
|
||||||
|
.open_table(crate::store::STORE_TABLE)
|
||||||
|
.expect("Failed to open STORE_TABLE");
|
||||||
|
|
||||||
let stores = vec![
|
let stores = vec![
|
||||||
("knowledge_graph_master", "knowledge_graph_master.json"),
|
("knowledge_graph_master", "knowledge_graph_master.json"),
|
||||||
@@ -777,13 +780,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (key, file_name) in stores.iter() {
|
for (key, file_name) in stores.iter() {
|
||||||
if table.get(*key).expect("Failed to read from table").is_none() {
|
if table
|
||||||
|
.get(*key)
|
||||||
|
.expect("Failed to read from table")
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
let json_path = base.join(file_name);
|
let json_path = base.join(file_name);
|
||||||
if json_path.exists()
|
if json_path.exists()
|
||||||
&& let Ok(data) = fs::read(&json_path)
|
&& let Ok(data) = fs::read(&json_path)
|
||||||
&& serde_json::from_slice::<serde_json::Value>(&data).is_ok()
|
&& serde_json::from_slice::<serde_json::Value>(&data).is_ok()
|
||||||
{
|
{
|
||||||
table.insert(*key, data.as_slice()).expect("Failed to insert migrated data");
|
table
|
||||||
|
.insert(*key, data.as_slice())
|
||||||
|
.expect("Failed to insert migrated data");
|
||||||
let _ = fs::rename(&json_path, json_path.with_extension("json.migrated"));
|
let _ = fs::rename(&json_path, json_path.with_extension("json.migrated"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,3 @@ pub fn tool_def<T: JsonSchema>(name: &str, description: &str) -> serde_json::Val
|
|||||||
"inputSchema": schema_val
|
"inputSchema": schema_val
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
+3
-1
@@ -101,7 +101,9 @@ impl MemoryState {
|
|||||||
idx.add_adr_sync(a);
|
idx.add_adr_sync(a);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).await.unwrap_or_else(|e| {
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
tracing::error!("Failed to join tantivy index rebuild thread: {}", e);
|
tracing::error!("Failed to join tantivy index rebuild thread: {}", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -170,5 +170,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in new issue
Block a user