From a866d8c36a9e567aa7dd9aef5c24cddaf1ed6311 Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Sun, 17 May 2026 01:42:27 +0000 Subject: [PATCH] fix filesystem read line limits --- .../__tests__/structured-content.test.ts | 47 +++++++++++++++++++ src/filesystem/index.ts | 23 +++++---- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/filesystem/__tests__/structured-content.test.ts b/src/filesystem/__tests__/structured-content.test.ts index 4b8f92b0a3..d95b365e7a 100644 --- a/src/filesystem/__tests__/structured-content.test.ts +++ b/src/filesystem/__tests__/structured-content.test.ts @@ -155,4 +155,51 @@ describe('structuredContent schema compliance', () => { expect(Array.isArray(structuredContent.content)).toBe(false); }); }); + + describe('read_text_file line limits', () => { + it('treats zero head and tail values as explicit limits', async () => { + const filePath = path.join(testDir, 'lines.txt'); + await fs.writeFile(filePath, 'line1\nline2\nline3\n'); + + const headResult = await client.callTool({ + name: 'read_text_file', + arguments: { path: filePath, head: 0 } + }); + expect(headResult.content).toEqual([{ type: 'text', text: '' }]); + expect((headResult.structuredContent as { content: unknown }).content).toBe(''); + + const tailResult = await client.callTool({ + name: 'read_text_file', + arguments: { path: filePath, tail: 0 } + }); + expect(tailResult.content).toEqual([{ type: 'text', text: '' }]); + expect((tailResult.structuredContent as { content: unknown }).content).toBe(''); + }); + + it('rejects head and tail when both are provided, even when zero', async () => { + const filePath = path.join(testDir, 'test.txt'); + + const result = await client.callTool({ + name: 'read_text_file', + arguments: { path: filePath, head: 0, tail: 0 } + }); + + expect(result.isError).toBe(true); + expect((result.content[0] as { text: string }).text) + .toContain('Cannot specify both head and tail parameters simultaneously'); + }); + + it('rejects fractional line limits', async () => { + const filePath = path.join(testDir, 'test.txt'); + + const result = await client.callTool({ + name: 'read_text_file', + arguments: { path: filePath, head: 1.5 } + }); + + expect(result.isError).toBe(true); + expect((result.content[0] as { text: string }).text) + .toContain('Expected integer'); + }); + }); }); diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..fa1b679566 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -93,10 +93,12 @@ allowedDirectories = accessibleDirectories; setAllowedDirectories(allowedDirectories); // Schema definitions +const LineLimitSchema = z.number().int().nonnegative(); + const ReadTextFileArgsSchema = z.object({ path: z.string(), - tail: z.number().optional().describe('If provided, returns only the last N lines of the file'), - head: z.number().optional().describe('If provided, returns only the first N lines of the file') + tail: LineLimitSchema.optional().describe('If provided, returns only the last N lines of the file'), + head: LineLimitSchema.optional().describe('If provided, returns only the first N lines of the file') }); const ReadMediaFileArgsSchema = z.object({ @@ -190,16 +192,19 @@ async function readFileAsBase64Stream(filePath: string): Promise { // read_file (deprecated) and read_text_file const readTextFileHandler = async (args: z.infer) => { const validPath = await validatePath(args.path); + const { head, tail } = args; + const hasHead = head !== undefined; + const hasTail = tail !== undefined; - if (args.head && args.tail) { + if (hasHead && hasTail) { throw new Error("Cannot specify both head and tail parameters simultaneously"); } let content: string; - if (args.tail) { - content = await tailFile(validPath, args.tail); - } else if (args.head) { - content = await headFile(validPath, args.head); + if (hasTail) { + content = await tailFile(validPath, tail); + } else if (hasHead) { + content = await headFile(validPath, head); } else { content = await readFileContent(validPath); } @@ -236,8 +241,8 @@ server.registerTool( "Only works within allowed directories.", inputSchema: { path: z.string(), - tail: z.number().optional().describe("If provided, returns only the last N lines of the file"), - head: z.number().optional().describe("If provided, returns only the first N lines of the file") + tail: LineLimitSchema.optional().describe("If provided, returns only the last N lines of the file"), + head: LineLimitSchema.optional().describe("If provided, returns only the first N lines of the file") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: true }