Skip to content

Commit 0f5f44f

Browse files
author
Abderraouf Belalia
committed
feat: add optional line numbering to read_text_file tool
Implements #60 - Add line numbering flag for read_text_file Changes: - Added `with_line_numbers` optional parameter to ReadTextFile struct - Updated read_text_file service method to format output with line numbers - Line numbers are right-aligned (6 digits) with pipe separator format - Uses 1-based indexing for line numbers - Maintains backward compatibility (defaults to false) - Added comprehensive unit tests for various scenarios - Updated CHANGELOG.md with feature description This feature enables AI agents to obtain file content with line numbers in a single tool invocation, improving efficiency for code modification tasks that require precise line-based targeting. [agent commit]
1 parent 45e8519 commit 0f5f44f

File tree

5 files changed

+111
-6
lines changed

5 files changed

+111
-6
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### 🚀 Features
6+
7+
* Add optional line numbering to read_text_file tool ([#60](https://github.com/rust-mcp-stack/rust-mcp-filesystem/issues/60))
8+
- Added `with_line_numbers` optional parameter to `read_text_file` tool
9+
- When enabled, prefixes each line with right-aligned line numbers and pipe separator
10+
- Useful for AI agents that need to target specific lines for code patches
11+
- Maintains backward compatibility with existing usage
12+
313
## [0.3.6](https://github.com/rust-mcp-stack/rust-mcp-filesystem/compare/v0.3.5...v0.3.6) (2025-10-15)
414

515

src/fs_service.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,11 +575,21 @@ impl FileSystemService {
575575
Ok(base64_string)
576576
}
577577

578-
pub async fn read_text_file(&self, file_path: &Path) -> ServiceResult<String> {
578+
pub async fn read_text_file(&self, file_path: &Path, with_line_numbers: bool) -> ServiceResult<String> {
579579
let allowed_directories = self.allowed_directories().await;
580580
let valid_path = self.validate_path(file_path, allowed_directories)?;
581581
let content = tokio::fs::read_to_string(valid_path).await?;
582-
Ok(content)
582+
583+
if with_line_numbers {
584+
Ok(content
585+
.lines()
586+
.enumerate()
587+
.map(|(i, line)| format!("{:>6} | {}", i + 1, line))
588+
.collect::<Vec<_>>()
589+
.join("\n"))
590+
} else {
591+
Ok(content)
592+
}
583593
}
584594

585595
pub async fn create_directory(&self, file_path: &Path) -> ServiceResult<()> {

src/tools/read_multiple_text_files.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl ReadMultipleTextFiles {
3535
.map(|path| async move {
3636
{
3737
let content = context
38-
.read_text_file(Path::new(&path))
38+
.read_text_file(Path::new(&path), false)
3939
.await
4040
.map_err(CallToolError::new);
4141

src/tools/read_text_file.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use crate::fs_service::FileSystemService;
1212
description = concat!("Read the complete contents of a text file from the file system as text. ",
1313
"Handles various text encodings and provides detailed error messages if the ",
1414
"file cannot be read. Use this tool when you need to examine the contents of ",
15-
"a single file. Only works within allowed directories."),
15+
"a single file. Optionally include line numbers for precise code targeting. ",
16+
"Only works within allowed directories."),
1617
destructive_hint = false,
1718
idempotent_hint = false,
1819
open_world_hint = false,
@@ -22,6 +23,11 @@ use crate::fs_service::FileSystemService;
2223
pub struct ReadTextFile {
2324
/// The path of the file to read.
2425
pub path: String,
26+
/// Optional: Include line numbers in output (default: false).
27+
/// When enabled, each line is prefixed with its line number (1-based).
28+
/// Useful for AI agents that need to target specific lines for code patches.
29+
#[serde(default)]
30+
pub with_line_numbers: Option<bool>,
2531
}
2632

2733
impl ReadTextFile {
@@ -30,7 +36,10 @@ impl ReadTextFile {
3036
context: &FileSystemService,
3137
) -> std::result::Result<CallToolResult, CallToolError> {
3238
let content = context
33-
.read_text_file(Path::new(&params.path))
39+
.read_text_file(
40+
Path::new(&params.path),
41+
params.with_line_numbers.unwrap_or(false),
42+
)
3443
.await
3544
.map_err(CallToolError::new)?;
3645

tests/test_fs_service.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,86 @@ async fn test_unzip_file_non_existent() {
230230
async fn test_read_file() {
231231
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);
232232
let file_path = create_temp_file(temp_dir.join("dir1").as_path(), "test.txt", "content");
233-
let content = service.read_text_file(&file_path).await.unwrap();
233+
let content = service.read_text_file(&file_path, false).await.unwrap();
234234
assert_eq!(content, "content");
235235
}
236236

237+
#[tokio::test]
238+
async fn test_read_text_file_with_line_numbers() {
239+
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);
240+
let file_path = create_temp_file(
241+
temp_dir.join("dir1").as_path(),
242+
"test.txt",
243+
"line1\nline2\nline3"
244+
);
245+
let content = service.read_text_file(&file_path, true).await.unwrap();
246+
assert_eq!(content, " 1 | line1\n 2 | line2\n 3 | line3");
247+
}
248+
249+
#[tokio::test]
250+
async fn test_read_text_file_without_line_numbers() {
251+
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);
252+
let file_path = create_temp_file(
253+
temp_dir.join("dir1").as_path(),
254+
"test.txt",
255+
"line1\nline2\nline3"
256+
);
257+
let content = service.read_text_file(&file_path, false).await.unwrap();
258+
assert_eq!(content, "line1\nline2\nline3");
259+
}
260+
261+
#[tokio::test]
262+
async fn test_read_text_file_with_line_numbers_empty_file() {
263+
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);
264+
let file_path = create_temp_file(temp_dir.join("dir1").as_path(), "empty.txt", "");
265+
let content = service.read_text_file(&file_path, true).await.unwrap();
266+
assert_eq!(content, "");
267+
}
268+
269+
#[tokio::test]
270+
async fn test_read_text_file_with_line_numbers_single_line() {
271+
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);
272+
let file_path = create_temp_file(temp_dir.join("dir1").as_path(), "single.txt", "single line");
273+
let content = service.read_text_file(&file_path, true).await.unwrap();
274+
assert_eq!(content, " 1 | single line");
275+
}
276+
277+
#[tokio::test]
278+
async fn test_read_text_file_with_line_numbers_no_trailing_newline() {
279+
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);
280+
let file_path = create_temp_file(
281+
temp_dir.join("dir1").as_path(),
282+
"no_newline.txt",
283+
"line1\nline2"
284+
);
285+
let content = service.read_text_file(&file_path, true).await.unwrap();
286+
assert_eq!(content, " 1 | line1\n 2 | line2");
287+
}
288+
289+
#[tokio::test]
290+
async fn test_read_text_file_with_line_numbers_large_file() {
291+
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);
292+
// Create a file with more than 999 lines to test padding
293+
let mut lines = Vec::new();
294+
for i in 1..=1000 {
295+
lines.push(format!("line{}", i));
296+
}
297+
let file_content = lines.join("\n");
298+
let file_path = create_temp_file(
299+
temp_dir.join("dir1").as_path(),
300+
"large.txt",
301+
&file_content
302+
);
303+
let content = service.read_text_file(&file_path, true).await.unwrap();
304+
305+
// Check first line
306+
assert!(content.starts_with(" 1 | line1\n"));
307+
// Check line 999
308+
assert!(content.contains(" 999 | line999\n"));
309+
// Check line 1000 (6 digits with right padding)
310+
assert!(content.contains(" 1000 | line1000"));
311+
}
312+
237313
#[tokio::test]
238314
async fn test_create_directory() {
239315
let (temp_dir, service, _allowed_dirs) = setup_service(vec!["dir1".to_string()]);

0 commit comments

Comments
 (0)