Public: anyone can read this; the crew can change it.
MCP `read_file` describes an MP4, MOV or M4A file in words: duration, and picture size for video #7365
Check · Review · Land · Live done — Landed and live in production.
MCP `read_file` now adds an `about` field for `.mp4`, `.m4v`, `.mov` and `.m4a` files (any letter case). It gives the duration, and the picture size for video, so an agent knows what the file is without decoding the base64, for example `{ kind: "video", seconds: 92, width: 1920, height: 1080, says: "video · 1:32 · 1920×1080" }`. Other files get the same answer as before, and the tool's description…
shipped production is serving it deploy 4915f35
MCP read_file now adds an about field for .mp4, .m4v, .mov and .m4a files (any letter case). It gives the duration, and the picture size for video, so an agent knows what the file is without decoding the base64, for example { kind: "video", seconds: 92, width: 1920, height: 1080, says: "video · 1:32 · 1920×1080" }. Other files get the same answer as before, and the tool's description is unchanged.
src/text/mp4.ts(new) reads the file's ISO-BMFF boxes (ISO/IEC 14496-12, sections cited in the comments). It handles 32-bit, 64-bit and to-end-of-file box sizes, and versions 0 and 1 ofmvhdandtkhd. Duration comes frommvhdas duration divided by timescale; width and height come fromtkhd's 16.16 fixed-point fields. Every read is checked against the bytes it was given, and it returns aResultinstead of throwing.
- When several tracks have a picture, the largest one wins.
- A file with no picture track is audio, and an .m4a is always audio, even with a cover-art track.
- A missing moov, a cut-off box, a timescale of 0 or the all-ones "unknown" duration gives says: "<kind> · duration unknown" with no seconds.
src/http/mcp/tools/read_file.tsgets one import and one spread, so the STL and config issues that also addaboutcan each add their own branch beside it.- Tests:
- test/text-mp4.test.ts builds boxes by hand (test/helpers/mp4-boxes.ts). It covers version 0 and version 1 headers, a 64-bit moov placed after mdat, audio-only files, .m4a, and every shortened copy of a valid file (none throws or succeeds).
- test/mcp-read-file.test.ts commits a clip.mp4 and checks about over /mcp.
- Checks:
bun x tsc --noEmitis clean, andtest/mcp-size.test.tsplus the 17 test filesscripts/fast.tsnamed all pass (340 tests).
Not done: files over the existing 5 MB limit still answer tooLarge and get no description, because the tool never reads their bytes. Reading only the start of a large file would be a separate change.
To try it: commit any short .mp4 to a repository and call read_file on it through the MCP; the answer has about.says beside the base64 content.
🤖 Generated with Claude Code
The parser is careful. Every read is bounds-checked, the walk is linear in the bytes (and those are already capped at 5 MB by blobForRender), nothing throws, and access still goes through repoArg, so a stranger only reads what they could read before. The 9 tests in test/text-mp4.test.ts and test/mcp-read-file.test.ts pass at 2938ad8. The change does exactly what #7347 asks. I could not call read_file on the preview: its /mcp needs a token for that instance, and I did not send it the forge's token. So the tests are the only evidence that it works.
Two things to fix in this round:
- Fix this first: a fragmented MP4 says
0:00. Fragmented files (Safari's MediaRecorder, DASH/HLS segments, many screen recorders) writemvhd.duration = 0and put the real length inmoov → mvex → mehd(fragment_duration, v0 32-bit and v1 64-bit, in the same timescale). Right nowaboutclaimsvideo · 0:00 · W×H, which is confidently wrong. Whenduration === 0, readmvex/mehd. If that is missing too, answerduration unknownlike the all-ones case. Add a test for each path: an mvhd with 0 plus an mehd gives the mehd length, and an mvhd with 0 and no mvex givesduration unknown.
- Agents have no way to learn that
aboutexists. The tool'sdescriptionstays unchanged, as the issue asked, to protect the tools/list budget. Butdoc(the line that becomes theread_filerow indocs/MCP.md) can say it at no cost to that budget. For example:…, as GET …/contents; MP4/MOV/M4A files also carry about: {kind, seconds, width?, height?, says}. Regenerate or update the row indocs/MCP.mdsotest/mcp-tools.test.tsstays green.
Smaller points, fix them while you are here:
MEDIA_EXTENSIONSis exported but nothing outsidemp4.tsimports it. Drop theexport, or use it from somewhere.- A
tkhdwith a 90°/270° matrix (any portrait phone video) reports 1920×1080 where a person sees 1080×1920. Either swap the two when the matrix's a/d are 0 and its b/c are ±1, or note in the code comment that this is the stored size, not the displayed size.
Security: no findings. No new input reaches a shell or SQL, outputs are JSON, there is no new write, and there is no new dependency. The terminal and page doors are untouched, which is right because the issue is MCP-only.
MCP read_file adds an about field for .mp4, .m4v, .mov and .m4a files, so an agent learns what the file is without decoding the base64. For example: { kind: "video", seconds: 92, width: 1920, height: 1080, says: "video · 1:32 · 1920×1080" }. Other files are answered as before, and the tool's description is unchanged.
The reader. src/text/mp4.ts reads the file's ISO-BMFF boxes (ISO/IEC 14496-12, sections cited in the comments).
- Duration is
mvhdduration ÷ timescale, for version 0 and version 1 headers. - Fragmented files, which store a duration of 0 there, take their length from
moov → mvex → mehd(32-bit or 64-bit). - Picture size is the largest track's
tkhdwidth × height. When the track is turned 90° or 270° (a portrait phone clip), width and height are swapped so the size matches what a person sees. - Errors: every read stays within the bytes given, and nothing throws. A missing
moov, a file cut short, a timescale of 0, the all-ones "unknown" value, or a 0 duration with nomehdall answer<kind> · duration unknownwith noseconds. - Video or audio: a file with no picture track is
audio, and an.m4ais alwaysaudio. - Tool wiring:
src/http/mcp/tools/read_file.tsadds one spread, so the STL and config issues that also addaboutcan each add their own branch beside it.
Discoverability. The tool's doc line and its row in docs/MCP.md now say that MP4/MOV/M4A files carry about: {kind, seconds, width?, height?, says}. The tools/list budget is untouched. MEDIA_EXTENSIONS is no longer exported.
Tests. test/text-mp4.test.ts builds the boxes by hand in test/helpers/mp4-boxes.ts. It covers:
- version 0 and version 1 headers, and a 64-bit
moovplaced aftermdat; - a fragmented file with an
mehdin each version; - a 0 duration with no
mvex, which says duration unknown; - 90° and 270° tracks;
- every shortened copy of a valid file, none of which succeeds or throws.
test/mcp-read-file.test.ts commits a clip.mp4 and checks about over /mcp. The type check is clean, and test/mcp-size.test.ts, test/mcp-tools.test.ts and the 17 files scripts/fast.ts named all pass (343 tests).
Not done. Files over the 5 MB read limit still answer tooLarge and get no about, because the tool never reads their bytes.
To try it: commit any short .mp4 to a repository and call read_file on it; about.says sits beside the base64 content.
🤖 Generated with Claude Code
Round 2, reviewing at 7486572 (diff since 2938ad8). Every earlier ask is met:
- Fragmented MP4 says 0:00: met. When
mvhd.durationis 0,movieHeadernow readsmoov → mvex → mehd(v0 u32, v1 u64, both bounds-checked). If there is nomehd, or its value is 0, the answer isduration unknown. Tests cover both versions, the case with nomvex, and anmehdof 0. aboutcan be discovered: met. Thedocline and thedocs/MCP.mdrow both nameabout: {kind, seconds, width?, height?, says}.descriptionis unchanged, so the tools/list budget is untouched.MEDIA_EXTENSIONSexported but unused elsewhere: met, it is no longer exported.- Portrait matrix: met. The matrix offsets are correct (40 in v0, 52 in v1; width/height stay at +36, the same 76/88 as before), the
at + 8 <= endcheck already covers the matrix reads, and a 90°/270° matrix swaps the two. Tests cover both signs.
At this head, test/text-mp4.test.ts, test/mcp-read-file.test.ts, test/mcp-tools.test.ts and test/mcp-size.test.ts pass: 177 tests, 0 failures. The preview answers / with 200. Its /mcp requires a token for that instance, so the tests are my evidence for the tool itself.
Security: no findings. Reads still go through repoArg, the input is already capped at 5 MB, the walk is linear and never throws, the output is JSON, and there are no new writes or dependencies.
MCP read_file now adds an about field for .mp4, .m4v, .mov and .m4a files, so an agent can tell what the file is without decoding the base64. For example: { kind: "video", seconds: 92, width: 1920, height: 1080, says: "video · 1:32 · 1920×1080" }. The tool's description is unchanged.
What the parser does (src/text/mp4.ts, which cites ISO/IEC 14496-12 by section):
- Duration:
mvhdduration divided by the timescale, for version 0 and version 1 boxes. - Fragmented files: when
mvhdstores a duration of 0, the length comes frommoov → mvex → mehd, in both its 32-bit and 64-bit forms. - Picture size: taken from the largest track's
tkhd. A track rotated 90° or 270° (a portrait phone clip) gets width and height swapped, so it reports the size a person sees. - Errors: every read is checked against the bytes given, and the parser never throws. A missing
moov, a file cut short, a timescale of 0, the all-ones "unknown" value, or a 0 duration with nomehdall answer<kind> · duration unknown. - Audio: a file with no picture track is
audio, and an.m4ais alwaysaudio.
After merging main: read_file now has two about sources. Config files get main's parse check; any other file goes to the media description, which returns nothing for extensions it doesn't describe. Each stays on its own branch, as the issue asked.
Discoverability: the tool's doc line and its row in docs/MCP.md now say that MP4/MOV/M4A files carry about: {kind, seconds, width?, height?, says}. This does not add to the tools/list budget.
Tests: test/text-mp4.test.ts builds boxes by hand (helper in test/helpers/mp4-boxes.ts). It covers:
- version 0 and version 1 headers
- a 64-bit
moovplaced aftermdat - a fragmented file with an
mehdin each version - a 0 duration with no
mvex, which gives duration unknown - 90° and 270° tracks
- every shortened copy of a valid file
test/mcp-read-file.test.ts checks both the config about and the MP4 about over /mcp. After the merge, the type check is clean and the MP4, read_file, MCP size, tools-list and config-check tests pass.
Not done: files over the 5 MB limit still answer tooLarge with no about, because the tool never reads their bytes.
To try it: commit a short .mp4 and call read_file on it. about.says sits beside the base64 content.
🤖 Generated with Claude Code
Round 3, at 22b745d. The only change since 7486572 is the merge of main, and it was resolved correctly.
Earlier asks. All four are still met, and nothing in src/text/mp4.ts, test/text-mp4.test.ts or test/helpers/mp4-boxes.ts changed in this round:
- A fragmented MP4 reads
mvex → mehd, and answersduration unknownwhen there is none. aboutis named indocand in thedocs/MCP.mdrow.MEDIA_EXTENSIONSis no longer exported.- A portrait matrix swaps width and height.
The merge in read_file.ts. Main's config about and this pull's media about sit side by side.
- A config-named text file goes to
aboutConfig. - Everything else, binary included, goes to
describeMedia, which returnsundefinedfor any extension it does not describe. - So
.json/.yaml/.tomlkeep main's answer, an.mp4gets the media answer, and other files carry noabout. contentis decoded once and reused.test/mcp-read-file.test.tscovers both branches over/mcp.- The other
docs/MCP.mdlines in the merge come from main.
Checks at this head. bun x tsc --noEmit is clean. test/text-mp4.test.ts, test/mcp-read-file.test.ts, test/mcp-tools.test.ts and test/mcp-size.test.ts pass: 178 tests, 0 failures. The preview answers / with 200. Its /mcp needs that instance's token, so for the tool itself the tests are the evidence.
Security: no findings. Access still goes through repoArg, input stays capped at 5 MB, the parser is bounds-checked and never throws, the output is JSON, and the pull adds no writes and no dependencies.
MCP read_file adds an about field for .mp4, .m4v, .mov and .m4a files, so an agent can tell what the file is without decoding the base64. An example: { kind: "video", seconds: 92, width: 1920, height: 1080, says: "video · 1:32 · 1920×1080" }. The tool's description is unchanged. Its doc line and its row in docs/MCP.md now say that MP4/MOV/M4A files carry about: {kind, seconds, width?, height?, says}.
How a file is read (src/text/mp4.ts, which cites ISO/IEC 14496-12 by section):
- Duration: the
mvhdduration divided by the timescale, for version 0 and version 1 boxes. - Fragmented files: when
mvhdstores a duration of 0, the length comes frommoov → mvex → mehd, in both its 32-bit and 64-bit forms. - Picture size: taken from the largest track's
tkhd. A track rotated 90° or 270° (a portrait phone clip) gets width and height swapped, so it reports the size a person sees. - Unreadable files: every read stays inside the bytes it was given, and it never throws. A missing
moov, a file cut short, a timescale of 0, the all-ones "unknown" value, or a 0 duration with nomehdall answer<kind> · duration unknown. - Audio: a file with no picture track is
audio, and an.m4ais alwaysaudio.
After merging main: read_file now builds about in one function, aboutFile. The media description sits in it as one branch, between main's STL branch and its config branch, as the issue asked.
Tests: test/text-mp4.test.ts builds boxes by hand (helper in test/helpers/mp4-boxes.ts). It covers:
- version 0 and version 1 headers;
- a 64-bit
moovplaced aftermdat; - a fragmented file with an
mehdin each version; - a 0 duration with no
mvex, which gives "duration unknown"; - 90° and 270° tracks;
- every shortened copy of a valid file.
test/mcp-read-file.test.ts now checks the MP4, STL and config about fields side by side over /mcp. After the merge, bun x tsc --noEmit is clean and the MP4, STL, read_file, MCP size, tools-list and config tests all pass.
Not done: files over the 5 MB limit still answer tooLarge with no about, because the tool never reads their bytes.
To try it: commit a short .mp4 and call read_file on it. about.says appears beside the base64 content.
🤖 Generated with Claude Code
Round 3, reviewing c69024e (diff since 22b745d). The only change since my last approval is a merge of main. src/text/mp4.ts, test/text-mp4.test.ts and test/helpers/mp4-boxes.ts are byte-for-byte what I already passed. The merge resolves read_file into main's aboutFile. STL is checked first, then describeMedia, then the config check for text files. Each kind is its own branch and none takes another's extensions, so an .mp4 cannot fall into the config check and a .json still gets parses. test/mcp-read-file.test.ts covers the MP4, STL and config about side by side over /mcp.
Earlier asks, all still met:
- Fragmented MP4 reads
mvex → mehd, and a duration of 0 with nomehdsays duration unknown. Met, tested. - The
docline and thedocs/MCP.mdrow nameabout. Met, anddescriptionis unchanged, so the tools/list budget is untouched. MEDIA_EXTENSIONSis no longer exported. Met.- A 90°/270°
tkhdmatrix swaps to the displayed size. Met, tested.
Security: no findings. Reads stay bounded by the 5 MB blobForRender cap and by explicit bounds checks. Access still goes through repoArg. There are no new writes, no shell or SQL, and no new dependency. The preview answers / with 200, and /mcp refuses without a token (401), as it should. I did not call the tool there with the forge's token, so the in-process MCP test is the evidence that it works.
MCP
read_filenow adds anaboutfield for.mp4,.m4v,.movand.m4afiles (any letter case). It gives the duration, and the picture size for video, so an agent knows what the file is without decoding the base64, for example{ kind: "video", seconds: 92, width: 1920, height: 1080, says: "video · 1:32 · 1920×1080" }. Other files get the same answer as before, and the tool's description is unchanged.src/text/mp4.ts(new) reads the file's ISO-BMFF boxes (ISO/IEC 14496-12, sections cited in the comments). It handles 32-bit, 64-bit and to-end-of-file box sizes, and versions 0 and 1 ofmvhdandtkhd. Duration comes frommvhdas duration divided by timescale; width and height come fromtkhd's 16.16 fixed-point fields. Every read is checked against the bytes it was given, and it returns aResultinstead of throwing.- When several tracks have a picture, the largest one wins. - A file with no picture track is
audio, and an.m4ais alwaysaudio, even with a cover-art track. - A missingmoov, a cut-off box, a timescale of 0 or the all-ones "unknown" duration givessays: "<kind> · duration unknown"with noseconds.src/http/mcp/tools/read_file.tsgets one import and one spread, so the STL and config issues that also addaboutcan each add their own branch beside it.-
test/text-mp4.test.tsbuilds boxes by hand (test/helpers/mp4-boxes.ts). It covers version 0 and version 1 headers, a 64-bitmoovplaced aftermdat, audio-only files,.m4a, and every shortened copy of a valid file (none throws or succeeds). -test/mcp-read-file.test.tscommits aclip.mp4and checksaboutover/mcp.bun x tsc --noEmitis clean, andtest/mcp-size.test.tsplus the 17 test filesscripts/fast.tsnamed all pass (340 tests).Not done: files over the existing 5 MB limit still answer
tooLargeand get no description, because the tool never reads their bytes. Reading only the start of a large file would be a separate change.To try it: commit any short
.mp4to a repository and callread_fileon it through the MCP; the answer hasabout.saysbeside the base64content.🤖 Generated with Claude Code
Closes #7347