Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/filesystem/__tests__/structured-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
23 changes: 14 additions & 9 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -190,16 +192,19 @@ async function readFileAsBase64Stream(filePath: string): Promise<string> {
// read_file (deprecated) and read_text_file
const readTextFileHandler = async (args: z.infer<typeof ReadTextFileArgsSchema>) => {
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);
}
Expand Down Expand Up @@ -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 }
Expand Down
Loading