feat(mcp/developer): accept -1 for insert_line number (#4112)

Signed-off-by: sings-to-bees-on-wednesdays <222684290+sings-to-bees-on-wednesdays@users.noreply.github.com>
This commit is contained in:
sings-to-bees-on-wednesdays
2025-08-20 13:43:00 +00:00
committed by GitHub
parent 240084c50f
commit ba0f55d14b
+96 -5
View File
@@ -389,7 +389,7 @@ impl DeveloperRouter {
To use the write command, you must specify `file_text` which will become the new content of the file. Be careful with To use the write command, you must specify `file_text` which will become the new content of the file. Be careful with
existing files! This is a full overwrite, so you must include everything - not just sections you are modifying. existing files! This is a full overwrite, so you must include everything - not just sections you are modifying.
To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end)
and `new_str` (the text to insert). and `new_str` (the text to insert).
To use the edit_file command, you must specify both `old_str` and `new_str` To use the edit_file command, you must specify both `old_str` and `new_str`
@@ -416,7 +416,7 @@ impl DeveloperRouter {
unique section of the original file, including any whitespace. Make sure to include enough context that the match is not unique section of the original file, including any whitespace. Make sure to include enough context that the match is not
ambiguous. The entire original string will be replaced with `new_str`. ambiguous. The entire original string will be replaced with `new_str`.
To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end)
and `new_str` (the text to insert). and `new_str` (the text to insert).
"#}.to_string(), "str_replace") "#}.to_string(), "str_replace")
}; };
@@ -446,7 +446,7 @@ impl DeveloperRouter {
}, },
"insert_line": { "insert_line": {
"type": "integer", "type": "integer",
"description": "The line number after which to insert the text (0 for beginning of file). This parameter is required when using the insert command." "description": "The line number after which to insert the text (0 for beginning of file, -1 for end of file). This parameter is required when using the insert command."
}, },
"old_str": {"type": "string"}, "old_str": {"type": "string"},
"new_str": {"type": "string"}, "new_str": {"type": "string"},
@@ -1039,7 +1039,7 @@ impl DeveloperRouter {
"Missing 'insert_line' parameter".to_string(), "Missing 'insert_line' parameter".to_string(),
None, None,
) )
})? as usize; })?;
let new_str = params let new_str = params
.get("new_str") .get("new_str")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
@@ -1442,7 +1442,7 @@ impl DeveloperRouter {
async fn text_editor_insert( async fn text_editor_insert(
&self, &self,
path: &PathBuf, path: &PathBuf,
insert_line: usize, insert_line_spec: i64,
new_str: &str, new_str: &str,
) -> Result<Vec<Content>, ErrorData> { ) -> Result<Vec<Content>, ErrorData> {
// Check if file exists // Check if file exists
@@ -1472,6 +1472,14 @@ impl DeveloperRouter {
let lines: Vec<&str> = content.lines().collect(); let lines: Vec<&str> = content.lines().collect();
let total_lines = lines.len(); let total_lines = lines.len();
// Allow insert_line to be negative
let insert_line = if insert_line_spec < 0 {
// -1 == end of file, -2 == before the last line, etc.
(total_lines as i64 + 1 + insert_line_spec) as usize
} else {
insert_line_spec as usize
};
// Validate insert_line parameter // Validate insert_line parameter
if insert_line > total_lines { if insert_line > total_lines {
return Err(ErrorData::new(ErrorCode::INVALID_PARAMS, format!( return Err(ErrorData::new(ErrorCode::INVALID_PARAMS, format!(
@@ -3270,6 +3278,89 @@ mod tests {
temp_dir.close().unwrap(); temp_dir.close().unwrap();
} }
#[tokio::test]
#[serial]
async fn test_text_editor_insert_at_end_negative() {
let router = get_router().await;
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
let file_path_str = file_path.to_str().unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
// Create a file with some content
let content = "Line 1\nLine 2\nLine 3";
router
.call_tool(
"text_editor",
json!({
"command": "write",
"path": file_path_str,
"file_text": content
}),
dummy_sender(),
)
.await
.unwrap();
// Insert at the end (after line 3)
let insert_result = router
.call_tool(
"text_editor",
json!({
"command": "insert",
"path": file_path_str,
"insert_line": -1,
"new_str": "Line 4"
}),
dummy_sender(),
)
.await
.unwrap();
let text = insert_result
.iter()
.find(|c| {
c.audience()
.is_some_and(|roles| roles.contains(&Role::Assistant))
})
.unwrap()
.as_text()
.unwrap();
assert!(text.text.contains("Text has been inserted at line 4"));
// Verify the file content
let view_result = router
.call_tool(
"text_editor",
json!({
"command": "view",
"path": file_path_str
}),
dummy_sender(),
)
.await
.unwrap();
let view_text = view_result
.iter()
.find(|c| {
c.audience()
.is_some_and(|roles| roles.contains(&Role::User))
})
.unwrap()
.as_text()
.unwrap();
assert!(view_text.text.contains("1: Line 1"));
assert!(view_text.text.contains("2: Line 2"));
assert!(view_text.text.contains("3: Line 3"));
assert!(view_text.text.contains("4: Line 4"));
temp_dir.close().unwrap();
}
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_text_editor_insert_invalid_line() { async fn test_text_editor_insert_invalid_line() {