filesystem: read_text_file with tail: N returns N-1 lines (or "" for tail: 1) when the file ends with a newline
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 79/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- node.js, typescript
- Ambito
- backend
Direzione di ricerca
Inizia da tailFile intorno alla riga 596 di src/filesystem/lib.ts, poi esamina il ciclo di lettura intorno alla riga 581 e confrontane il comportamento con quello di headFile. Riproduci il problema usando l’esempio autonomo fornito, soprattutto con file più grandi del blocco da 1 KB e con terminazioni di riga LF e CRLF. Il lavoro è completato quando tail: N restituisce le ultime N righe complete senza un carattere di nuova riga finale superfluo, anche per i file che terminano con una nuova riga.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Which server?
everything (@modelcontextprotocol/server-everything)
Server version
@modelcontextprotocol/server-filesystem 1.0.0 (npm)
How did you run the server?
npx (npm package)
Transport
stdio
Protocol era
Modern (2026-07-28)
MCP client and version
pytest-mcptest 0.1.1 (built on the official mcp Python SDK 2.3.0), over stdio. The bug doesn't depend on the client: the standalone repro in "Steps to reproduce" calls tailFile directly and shows the same result without any MCP client.
Operating system and runtime
Linux Node version: 22.22.0
Server configuration
Summary
read_text_file with tail: N treats the empty string after a file's final \n as a line. Files ending in a newline (almost every text file) get one line fewer than requested, plus a stray trailing \n. tail: 1 returns an empty string.
PR #4977 pins this as a quirk ("tail counts the trailing newline as a line"), but I couldn't find an issue for it. The tool description says tail "returns only the last N lines of the file", and head: N on the same file does return N lines, so this looks like a bug rather than intended behavior.
Steps to reproduce
- Create a file that ends with a newline (as most text files do):
printf 'alpha\nbeta\ngamma\n' > /tmp/allowed/repro.txt - Start the server with that directory allowed:
npx -y @modelcontextprotocol/[email protected] /tmp/allowed - Call
read_text_filewith{"path": "/tmp/allowed/repro.txt", "tail": 1}.- Expected:
"gamma" - Actual:
""
- Expected:
- Call it again with
"tail": 2.- Expected:
"beta\ngamma" - Actual:
"gamma\n"(only 1 line)
- Expected:
- For comparison,
"head": 2correctly returns"alpha\nbeta".
Minimal standalone repro, without an MCP client (run in an empty folder):
npm install @modelcontextprotocol/[email protected]
printf 'alpha\nbeta\ngamma\n' > repro.txt
node --input-type=module -e '
import { tailFile, headFile } from "@modelcontextprotocol/server-filesystem/dist/lib.js";
for (const n of [1, 2, 3]) console.log("tail", n, JSON.stringify(await tailFile("repro.txt", n)));
console.log("head 2", JSON.stringify(await headFile("repro.txt", 2)));
'
Output:
tail 1 ""
tail 2 "gamma\n"
tail 3 "beta\ngamma\n"
head 2 "alpha\nbeta"
Expected behavior
read_text_file with tail: N returns the last N lines of the file, as the tool description says ("returns only the last N lines of the file"). A trailing newline ends the final line; it does not start an extra, empty one. This matches head: N, which returns N lines with no trailing newline.
For a file containing alpha\nbeta\ngamma\n:
tail: 1→"gamma"tail: 2→"beta\ngamma"tail: 3→"alpha\nbeta\ngamma"
Actual behavior
When the file ends with a newline, tail counts the empty string after the final \n as a line. So it returns N-1 real lines plus a stray trailing \n, and tail: 1 returns an empty string:
tail: 1→""tail: 2→"gamma\n"(1 line instead of 2)tail: 3→"beta\ngamma\n"(2 lines instead of 3)
Files without a trailing newline are unaffected. Because most text files end with a newline, tail returns one line fewer than requested for almost every file.
Cause: tailFile in src/filesystem/lib.ts (line 596) does text.split("\n").slice(-numLines), and the split leaves a trailing "" element. Simply stripping the trailing newline isn't a complete fix: the read loop (line 581) stops after numLines newlines, so the first returned line can be cut off on files larger than the 1 KB chunk. The full fix and a 2,000-file differential test are in the "Additional context" section below.
Logs or error output
Already prototyped a fix?
Read until there is one more newline than lines wanted, and drop a single trailing newline before splitting:
-
while (position > 0 && newlinesFound < numLines) {
-
// One more newline than lines wanted, so the earliest returned line is -
// complete even when the file ends with a newline. -
while (position > 0 && newlinesFound <= numLines) {
@@
-
const text = normalizeLineEndings(Buffer.concat(chunks).toString("utf-8"));
-
let text = normalizeLineEndings(Buffer.concat(chunks).toString("utf-8")); -
// A trailing newline terminates the last line; it does not start a new, empty one. -
if (text.endsWith("\n")) -
text = text.slice(0, -1); return text.split("\n").slice(-numLines).join("\n");
This makes tail match head: neither result ends with a newline. The cost is reading at most one extra line from disk.
I ran a differential test with 2,000 random files against a reference ("last N lines; a trailing newline ends the final line"). The files had 1–40 lines, some lines longer than the 1 KB chunk, a mix of LF and CRLF, and about two-thirds ended with a trailing newline.
Before you submit
- I searched existing issues and this is not a duplicate.
- This is not a security vulnerability report (those go through the private advisory form).
- Lingua principale
- TypeScript
- Stelle
- 91.4k
- Fork
- 11.8k
- Merge medio
- 7h 36m
- PR unite (30g)
- 126
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di modelcontextprotocol/servers
-
filesystem: search_files does not expand non-wildcard excludePatterns like directory_tree doesAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
modelcontextprotocol/servers#5216 ·
I maintainer di solito rispondono entro 1 giorno
-
documentation server-git server-time v2
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
modelcontextprotocol/servers#5213 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
documentation v2
Difficoltà 1/5 1-3 ore Idoneità per principianti 78/100
modelcontextprotocol/servers#5212 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
documentation v2
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
modelcontextprotocol/servers#5211 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
chore server-everything v2
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
modelcontextprotocol/servers#5210 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di modelcontextprotocol/servers
Issue simili
-
Edit: Tele Posadas HDApertacheck:passed streams:edit
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno
-
area:mcp bug has repro platform:vscode regression
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
anthropics/claude-code#103970 ·
I maintainer di solito rispondono entro 1 giorno
-
skillopt: success-reflect prompt omits the edit schema, so every success-only edit is droppedAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Workspace secrets/constants containing "$" are corrupted when resolved in data source optionsApertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
I maintainer di solito rispondono entro 1 giorno
-
sdk-typescript: unquoted COPY globs in shell form are silently dropped from the image build contextApertabug javascript
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno