Compare commits

...
2 Commits
3 changed files with 101 additions and 10 deletions

No files matched your search

+91
View File
@@ -999,6 +999,31 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
"type": "object",
"properties": {}
}
},
{
"name": "nvim_read_file",
"description": "Read specific lines from a file buffer (like cat or sed -n). Reads unsaved changes if open.",
"inputSchema": {
"type": "object",
"properties": {
"file": { "type": "string" },
"start_line": { "type": "integer", "description": "1-indexed start line (optional, defaults to 1)" },
"end_line": { "type": "integer", "description": "1-indexed end line (optional, defaults to end of file)" }
},
"required": ["file"]
}
},
{
"name": "nvim_search_file",
"description": "Search for a regex pattern within a file buffer (like grep). Searches unsaved changes if open.",
"inputSchema": {
"type": "object",
"properties": {
"file": { "type": "string" },
"pattern": { "type": "string", "description": "Lua-compatible regex pattern to search for" }
},
"required": ["file", "pattern"]
}
}
]
})),
@@ -1330,6 +1355,72 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
}
}
"nvim_read_file" => {
if let Some(file) = args.get("file").and_then(|v| v.as_str()) {
let start_line = args.get("start_line").and_then(|v| v.as_i64()).unwrap_or(1);
let end_line = args.get("end_line").and_then(|v| v.as_i64()).unwrap_or(-1);
let escaped_file = file.replace('\\', "\\\\").replace('"', "\\\"");
let code = format!("
local file = \"{escaped_file}\"
local start_idx = {start_line} - 1
local end_idx = {end_line}
local bufnr = vim.fn.bufnr(file, true)
if vim.fn.bufloaded(bufnr) == 0 then
vim.fn.bufload(bufnr)
end
local lines = vim.api.nvim_buf_get_lines(bufnr, start_idx, end_idx, false)
local res = {{}}
for i, line in ipairs(lines) do
local line_num = start_idx + i
table.insert(res, tostring(line_num) .. ': ' .. line)
end
return table.concat(res, '\\n')
");
match execute_nvim_lua(&code).await {
Ok(res) => send_text_result!(id.clone(), res),
Err(e) => send_error(id, -32603, &e).await,
}
} else {
send_error(id, -32602, "Missing 'file'").await;
}
}
"nvim_search_file" => {
if let (Some(file), Some(pattern)) = (
args.get("file").and_then(|v| v.as_str()),
args.get("pattern").and_then(|v| v.as_str()),
) {
let escaped_file = file.replace('\\', "\\\\").replace('"', "\\\"");
let escaped_pattern = pattern.replace('\\', "\\\\").replace('"', "\\\"");
let code = format!("
local file = \"{escaped_file}\"
local pattern = \"{escaped_pattern}\"
local bufnr = vim.fn.bufnr(file, true)
if vim.fn.bufloaded(bufnr) == 0 then
vim.fn.bufload(bufnr)
end
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local matches = {{}}
for i, line in ipairs(lines) do
if string.match(line, pattern) then
table.insert(matches, tostring(i) .. ': ' .. line)
end
end
if #matches == 0 then
return 'No matches found.'
end
return table.concat(matches, '\\n')
");
match execute_nvim_lua(&code).await {
Ok(res) => send_text_result!(id.clone(), res),
Err(e) => send_error(id, -32603, &e).await,
}
} else {
send_error(id, -32602, "Missing 'file' or 'pattern'").await;
}
}
"nvim_execute_lua" => {
if let Some(code) = args.get("code").and_then(|v| v.as_str()) {
// BAKE IN: Block interactive prompts that cause server deadlocks
+9 -9
View File
@@ -631,7 +631,7 @@ mod tests {
let create_handler = CreateEntitiesHandler;
let args = json!({
"entities": [
{"name": "Alice", "entityType": "Person", "observations": ["Likes Bob"]}
{"name": "Alice", "entity_type": "Person", "observations": ["Likes Bob"]}
]
});
@@ -677,7 +677,7 @@ mod tests {
let handler = CreateRelationsHandler;
let args = json!({
"relations": [
{"from": "A", "to": "B", "relationType": "knows"}
{"from": "A", "to": "B", "relation_type": "knows"}
]
});
let res = handler.execute(args, state.clone()).await.unwrap();
@@ -706,7 +706,7 @@ mod tests {
let add_obs = AddObservationsHandler;
let args_obs = json!({
"observations": [
{"entityName": "A", "contents": ["Obs 1", "Obs 2"]}
{"entity_name": "A", "contents": ["Obs 1", "Obs 2"]}
]
});
let res1 = add_obs.execute(args_obs, state.clone()).await.unwrap();
@@ -722,7 +722,7 @@ mod tests {
let del_entity = DeleteEntitiesHandler;
let res4 = del_entity
.execute(json!({"entityNames": ["A"]}), state.clone())
.execute(json!({"entity_names": ["A"]}), state.clone())
.await
.unwrap();
assert_eq!(res4, "Entities deleted");
@@ -742,8 +742,8 @@ mod tests {
let create_handler = CreateEntitiesHandler;
let args_ent = json!({
"entities": [
{"name": "X", "entityType": "File", "observations": ["Obs X"], "namespace": "global"},
{"name": "Y", "entityType": "File", "observations": ["Obs Y"], "namespace": "global"}
{"name": "X", "entity_type": "File", "observations": ["Obs X"], "namespace": "global"},
{"name": "Y", "entity_type": "File", "observations": ["Obs Y"], "namespace": "global"}
]
});
create_handler
@@ -754,7 +754,7 @@ mod tests {
let rel_handler = CreateRelationsHandler;
let args_rel = json!({
"relations": [
{"from": "X", "to": "Y", "relationType": "depends_on", "namespace": "global"}
{"from": "X", "to": "Y", "relation_type": "depends_on", "namespace": "global"}
]
});
rel_handler.execute(args_rel, state.clone()).await.unwrap();
@@ -784,7 +784,7 @@ mod tests {
let condense = CondenseEntityHandler;
let res_cond = condense
.execute(
json!({"entityName": "X", "summarized_observations": ["X condensed"]}),
json!({"entity_name": "X", "summarized_observations": ["X condensed"]}),
state.clone(),
)
.await
@@ -794,7 +794,7 @@ mod tests {
let merge = MergeEntitiesHandler;
let res_merge = merge
.execute(
json!({"sourceEntity": "X", "targetEntity": "Y"}),
json!({"source_entity": "X", "target_entity": "Y"}),
state.clone(),
)
.await
+1 -1
View File
@@ -609,7 +609,7 @@ mod tests {
let code_handler = LogCodeChangeHandler;
let args_code = json!({
"filePath": "main.rs",
"file_path": "main.rs",
"description": "refactor",
"git_commit": "def",
"git_branch": "main"