@@ -30,6 +30,8 @@ const ParamsSchema = z.object({
3030 maxDepth : z . number ( ) . default ( 3 ) . describe ( 'Max directory depth to traverse' ) ,
3131 includeContent : z . boolean ( ) . default ( true ) . describe ( 'Include file content in result' ) ,
3232 maxFiles : z . number ( ) . default ( MAX_FILES ) . describe ( 'Max number of files to return' ) ,
33+ offset : z . number ( ) . min ( 0 ) . optional ( ) . describe ( 'Line offset to start reading from (0-based, for single file only)' ) ,
34+ limit : z . number ( ) . min ( 1 ) . optional ( ) . describe ( 'Number of lines to read (for single file only)' ) ,
3335} ) ;
3436
3537type Params = z . infer < typeof ParamsSchema > ;
@@ -40,6 +42,8 @@ interface FileEntry {
4042 content ?: string ;
4143 truncated ?: boolean ;
4244 matches ?: string [ ] ;
45+ totalLines ?: number ;
46+ lineRange ?: { start : number ; end : number } ;
4347}
4448
4549interface ReadResult {
@@ -123,23 +127,69 @@ function collectFiles(
123127 return files ;
124128}
125129
130+ interface ReadContentOptions {
131+ maxLength : number ;
132+ offset ?: number ;
133+ limit ?: number ;
134+ }
135+
136+ interface ReadContentResult {
137+ content : string ;
138+ truncated : boolean ;
139+ totalLines ?: number ;
140+ lineRange ?: { start : number ; end : number } ;
141+ }
142+
126143/**
127- * Read file content with truncation
144+ * Read file content with truncation and optional line-based pagination
128145 */
129- function readFileContent ( filePath : string , maxLength : number ) : { content : string ; truncated : boolean } {
146+ function readFileContent ( filePath : string , options : ReadContentOptions ) : ReadContentResult {
147+ const { maxLength, offset, limit } = options ;
148+
130149 if ( isBinaryFile ( filePath ) ) {
131150 return { content : '[Binary file]' , truncated : false } ;
132151 }
133152
134153 try {
135154 const content = readFileSync ( filePath , 'utf8' ) ;
155+ const lines = content . split ( '\n' ) ;
156+ const totalLines = lines . length ;
157+
158+ // If offset/limit specified, use line-based pagination
159+ if ( offset !== undefined || limit !== undefined ) {
160+ const startLine = Math . min ( offset ?? 0 , totalLines ) ;
161+ const endLine = limit !== undefined ? Math . min ( startLine + limit , totalLines ) : totalLines ;
162+ const selectedLines = lines . slice ( startLine , endLine ) ;
163+ const selectedContent = selectedLines . join ( '\n' ) ;
164+
165+ const actualEnd = endLine ;
166+ const hasMore = actualEnd < totalLines ;
167+
168+ let finalContent = selectedContent ;
169+ if ( selectedContent . length > maxLength ) {
170+ finalContent = selectedContent . substring ( 0 , maxLength ) + `\n... (+${ selectedContent . length - maxLength } chars)` ;
171+ }
172+
173+ // Calculate actual line range (handle empty selection)
174+ const actualLineEnd = selectedLines . length > 0 ? startLine + selectedLines . length - 1 : startLine ;
175+
176+ return {
177+ content : finalContent ,
178+ truncated : hasMore || selectedContent . length > maxLength ,
179+ totalLines,
180+ lineRange : { start : startLine , end : actualLineEnd } ,
181+ } ;
182+ }
183+
184+ // Default behavior: truncate by character length
136185 if ( content . length > maxLength ) {
137186 return {
138187 content : content . substring ( 0 , maxLength ) + `\n... (+${ content . length - maxLength } chars)` ,
139- truncated : true
188+ truncated : true ,
189+ totalLines,
140190 } ;
141191 }
142- return { content, truncated : false } ;
192+ return { content, truncated : false , totalLines } ;
143193 } catch ( error ) {
144194 return { content : `[Error: ${ ( error as Error ) . message } ]` , truncated : false } ;
145195 }
@@ -171,15 +221,17 @@ function findMatches(content: string, pattern: string): string[] {
171221// Tool schema for MCP
172222export const schema : ToolSchema = {
173223 name : 'read_file' ,
174- description : `Read files with multi-file, directory, and regex support.
224+ description : `Read files with multi-file, directory, regex support, and line-based pagination .
175225
176226Usage:
177- read_file(paths="file.ts") # Single file
178- read_file(paths=["a.ts", "b.ts"]) # Multiple files
179- read_file(paths="src/", pattern="*.ts") # Directory with pattern
180- read_file(paths="src/", contentPattern="TODO") # Search content
181-
182- Returns compact file list with optional content.` ,
227+ read_file(paths="file.ts") # Single file (full content)
228+ read_file(paths="file.ts", offset=100, limit=50) # Lines 100-149 (0-based)
229+ read_file(paths=["a.ts", "b.ts"]) # Multiple files
230+ read_file(paths="src/", pattern="*.ts") # Directory with pattern
231+ read_file(paths="src/", contentPattern="TODO") # Search content
232+
233+ Supports both absolute and relative paths. Relative paths are resolved from project root.
234+ Returns compact file list with optional content. Use offset/limit for large file pagination.` ,
183235 inputSchema : {
184236 type : 'object' ,
185237 properties : {
@@ -213,6 +265,16 @@ Returns compact file list with optional content.`,
213265 description : `Max number of files to return (default: ${ MAX_FILES } )` ,
214266 default : MAX_FILES ,
215267 } ,
268+ offset : {
269+ type : 'number' ,
270+ description : 'Line offset to start reading from (0-based, for single file only)' ,
271+ minimum : 0 ,
272+ } ,
273+ limit : {
274+ type : 'number' ,
275+ description : 'Number of lines to read (for single file only)' ,
276+ minimum : 1 ,
277+ } ,
216278 } ,
217279 required : [ 'paths' ] ,
218280 } ,
@@ -232,6 +294,8 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
232294 maxDepth,
233295 includeContent,
234296 maxFiles,
297+ offset,
298+ limit,
235299 } = parsed . data ;
236300
237301 const cwd = getProjectRoot ( ) ;
@@ -271,6 +335,10 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
271335 const files : FileEntry [ ] = [ ] ;
272336 let totalContent = 0 ;
273337
338+ // Only apply offset/limit for single file mode
339+ const isSingleFile = limitedFiles . length === 1 ;
340+ const useLinePagination = isSingleFile && ( offset !== undefined || limit !== undefined ) ;
341+
274342 for ( const filePath of limitedFiles ) {
275343 if ( totalContent >= MAX_TOTAL_CONTENT ) break ;
276344
@@ -283,7 +351,15 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
283351 if ( includeContent ) {
284352 const remainingSpace = MAX_TOTAL_CONTENT - totalContent ;
285353 const maxLen = Math . min ( MAX_CONTENT_LENGTH , remainingSpace ) ;
286- const { content, truncated } = readFileContent ( filePath , maxLen ) ;
354+
355+ // Pass offset/limit only for single file mode
356+ const readOptions : ReadContentOptions = { maxLength : maxLen } ;
357+ if ( useLinePagination ) {
358+ if ( offset !== undefined ) readOptions . offset = offset ;
359+ if ( limit !== undefined ) readOptions . limit = limit ;
360+ }
361+
362+ const { content, truncated, totalLines, lineRange } = readFileContent ( filePath , readOptions ) ;
287363
288364 // If contentPattern provided, only include files with matches
289365 if ( contentPattern ) {
@@ -292,13 +368,17 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
292368 entry . matches = matches ;
293369 entry . content = content ;
294370 entry . truncated = truncated ;
371+ entry . totalLines = totalLines ;
372+ entry . lineRange = lineRange ;
295373 totalContent += content . length ;
296374 } else {
297375 continue ; // Skip files without matches
298376 }
299377 } else {
300378 entry . content = content ;
301379 entry . truncated = truncated ;
380+ entry . totalLines = totalLines ;
381+ entry . lineRange = lineRange ;
302382 totalContent += content . length ;
303383 }
304384 }
@@ -311,6 +391,10 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
311391 if ( totalFiles > maxFiles ) {
312392 message += ` (showing ${ maxFiles } of ${ totalFiles } )` ;
313393 }
394+ if ( useLinePagination && files . length > 0 && files [ 0 ] . lineRange ) {
395+ const { start, end } = files [ 0 ] . lineRange ;
396+ message += ` [lines ${ start } -${ end } of ${ files [ 0 ] . totalLines } ]` ;
397+ }
314398 if ( contentPattern ) {
315399 message += ` matching "${ contentPattern } "` ;
316400 }
0 commit comments