@@ -285,6 +285,7 @@ impl LazyTailMcp {
285285 pub ( crate ) fn get_tail_impl (
286286 path : & Path ,
287287 count : usize ,
288+ since_line : Option < usize > ,
288289 raw : bool ,
289290 output : OutputFormat ,
290291 ) -> String {
@@ -300,10 +301,19 @@ impl LazyTailMcp {
300301 let index_reader = IndexReader :: open ( path) ;
301302
302303 let total = reader. total_lines ( ) ;
303- let start = total. saturating_sub ( count) ;
304+
305+ let ( start, end, has_more) = if let Some ( since) = since_line {
306+ let start = since. saturating_add ( 1 ) ;
307+ let end = start. saturating_add ( count) . min ( total) ;
308+ let has_more = end < total;
309+ ( start, end, has_more)
310+ } else {
311+ let start = total. saturating_sub ( count) ;
312+ ( start, total, start > 0 )
313+ } ;
304314
305315 let mut lines = Vec :: new ( ) ;
306- for i in start..total {
316+ for i in start..end {
307317 if let Ok ( Some ( content) ) = reader. get_line ( i) {
308318 lines. push ( LineInfo {
309319 line_number : i,
@@ -319,7 +329,7 @@ impl LazyTailMcp {
319329 let mut response = GetLinesResponse {
320330 lines,
321331 total_lines : total,
322- has_more : start > 0 ,
332+ has_more,
323333 } ;
324334
325335 if !raw {
@@ -713,14 +723,14 @@ impl LazyTailMcp {
713723
714724 /// Fetch the last N lines from a lazytail source.
715725 #[ tool(
716- description = "Fetch the last N lines from a lazytail-captured log source. Useful for checking recent activity. Pass a source name from list_sources. Returns up to 1000 lines from the end of the file."
726+ description = "Fetch the last N lines from a lazytail-captured log source. Useful for checking recent activity. Pass a source name from list_sources. Returns up to 1000 lines from the end of the file. Supports incremental polling via since_line — pass the last line_number you received to get only new lines added after that point. "
717727 ) ]
718728 fn get_tail ( & self , #[ tool( aggr) ] req : GetTailRequest ) -> String {
719729 let path = match source:: resolve_source_for_context ( & req. source , & self . discovery ) {
720730 Ok ( p) => p,
721731 Err ( e) => return error_response ( e) ,
722732 } ;
723- Self :: get_tail_impl ( & path, req. count , req. raw , req. output )
733+ Self :: get_tail_impl ( & path, req. count , req. since_line , req . raw , req. output )
724734 }
725735
726736 /// Search for patterns in a lazytail source using plain text, regex, or structured query.
@@ -925,7 +935,7 @@ plain line with no escapes\n\
925935 #[ test]
926936 fn get_tail_strips_ansi_by_default ( ) {
927937 let f = write_ansi_tempfile ( ) ;
928- let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 2 , false , OutputFormat :: Json ) ;
938+ let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 2 , None , false , OutputFormat :: Json ) ;
929939 let resp: GetLinesResponse = serde_json:: from_str ( & result) . unwrap ( ) ;
930940 assert_eq ! ( resp. lines. len( ) , 2 ) ;
931941 assert_eq ! ( resp. lines[ 0 ] . content, "plain line with no escapes" ) ;
@@ -935,11 +945,83 @@ plain line with no escapes\n\
935945 #[ test]
936946 fn get_tail_preserves_ansi_when_raw ( ) {
937947 let f = write_ansi_tempfile ( ) ;
938- let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 5 , true , OutputFormat :: Json ) ;
948+ let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 5 , None , true , OutputFormat :: Json ) ;
939949 let resp: GetLinesResponse = serde_json:: from_str ( & result) . unwrap ( ) ;
940950 assert ! ( resp. lines[ 0 ] . content. contains( "\x1b [" ) ) ;
941951 }
942952
953+ #[ test]
954+ fn get_tail_since_line_returns_new_lines ( ) {
955+ let mut f = NamedTempFile :: new ( ) . unwrap ( ) ;
956+ for i in 0 ..5 {
957+ writeln ! ( f, "line {i}" ) . unwrap ( ) ;
958+ }
959+ f. flush ( ) . unwrap ( ) ;
960+
961+ let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 100 , Some ( 2 ) , false , OutputFormat :: Json ) ;
962+ let resp: GetLinesResponse = serde_json:: from_str ( & result) . unwrap ( ) ;
963+
964+ assert_eq ! ( resp. lines. len( ) , 2 ) ;
965+ assert_eq ! ( resp. lines[ 0 ] . line_number, 3 ) ;
966+ assert_eq ! ( resp. lines[ 0 ] . content, "line 3" ) ;
967+ assert_eq ! ( resp. lines[ 1 ] . line_number, 4 ) ;
968+ assert_eq ! ( resp. lines[ 1 ] . content, "line 4" ) ;
969+ assert_eq ! ( resp. total_lines, 5 ) ;
970+ assert ! ( !resp. has_more) ;
971+ }
972+
973+ #[ test]
974+ fn get_tail_since_line_with_count ( ) {
975+ let mut f = NamedTempFile :: new ( ) . unwrap ( ) ;
976+ for i in 0 ..10 {
977+ writeln ! ( f, "line {i}" ) . unwrap ( ) ;
978+ }
979+ f. flush ( ) . unwrap ( ) ;
980+
981+ let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 2 , Some ( 5 ) , false , OutputFormat :: Json ) ;
982+ let resp: GetLinesResponse = serde_json:: from_str ( & result) . unwrap ( ) ;
983+
984+ assert_eq ! ( resp. lines. len( ) , 2 ) ;
985+ assert_eq ! ( resp. lines[ 0 ] . line_number, 6 ) ;
986+ assert_eq ! ( resp. lines[ 0 ] . content, "line 6" ) ;
987+ assert_eq ! ( resp. lines[ 1 ] . line_number, 7 ) ;
988+ assert_eq ! ( resp. lines[ 1 ] . content, "line 7" ) ;
989+ assert ! ( resp. has_more) ;
990+ }
991+
992+ #[ test]
993+ fn get_tail_since_line_at_end ( ) {
994+ let mut f = NamedTempFile :: new ( ) . unwrap ( ) ;
995+ for i in 0 ..5 {
996+ writeln ! ( f, "line {i}" ) . unwrap ( ) ;
997+ }
998+ f. flush ( ) . unwrap ( ) ;
999+
1000+ let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 100 , Some ( 4 ) , false , OutputFormat :: Json ) ;
1001+ let resp: GetLinesResponse = serde_json:: from_str ( & result) . unwrap ( ) ;
1002+
1003+ assert ! ( resp. lines. is_empty( ) ) ;
1004+ assert ! ( !resp. has_more) ;
1005+ assert_eq ! ( resp. total_lines, 5 ) ;
1006+ }
1007+
1008+ #[ test]
1009+ fn get_tail_since_line_beyond_end ( ) {
1010+ let mut f = NamedTempFile :: new ( ) . unwrap ( ) ;
1011+ for i in 0 ..5 {
1012+ writeln ! ( f, "line {i}" ) . unwrap ( ) ;
1013+ }
1014+ f. flush ( ) . unwrap ( ) ;
1015+
1016+ let result =
1017+ LazyTailMcp :: get_tail_impl ( f. path ( ) , 100 , Some ( 100 ) , false , OutputFormat :: Json ) ;
1018+ let resp: GetLinesResponse = serde_json:: from_str ( & result) . unwrap ( ) ;
1019+
1020+ assert ! ( resp. lines. is_empty( ) ) ;
1021+ assert ! ( !resp. has_more) ;
1022+ assert_eq ! ( resp. total_lines, 5 ) ;
1023+ }
1024+
9431025 // -- search tests --
9441026
9451027 #[ test]
@@ -1181,7 +1263,7 @@ plain line with no escapes\n\
11811263 writeln ! ( f, "line 2" ) . unwrap ( ) ;
11821264 f. flush ( ) . unwrap ( ) ;
11831265
1184- let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 2 , false , OutputFormat :: Text ) ;
1266+ let result = LazyTailMcp :: get_tail_impl ( f. path ( ) , 2 , None , false , OutputFormat :: Text ) ;
11851267 assert ! ( result. starts_with( "--- " ) ) ;
11861268 assert ! ( result. contains( "--- has_more: true\n " ) ) ;
11871269 assert ! ( result. contains( "[L1] line 1\n " ) ) ;
0 commit comments