fix(filesystem): return valid MCP content type from read_media_file (#4029)#4479
Merged
olaservo merged 3 commits intoJul 6, 2026
Conversation
…odelcontextprotocol#4029) read_media_file returned a content block with type: "blob" for non-image/ non-audio files. "blob" is not a valid MCP content type (the union is text | image | audio | resource_link | resource), so a strict client rejects the result on schema validation. Map the MIME type to a valid block: image/* -> image, audio/* -> audio, and any other file type -> an embedded resource (the spec-correct carrier for arbitrary bytes) with a pathToFileURL-encoded uri. Widen the tool's outputSchema to the matching union and drop the now-unnecessary `as unknown as CallToolResult` cast so tsc enforces the returned shape. Tests cover the image, audio, and resource content types, verify the base64 blob round-trips to the original bytes, and assert the resource uri is percent-encoded (space + non-ASCII) rather than a raw file:// concatenation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dia_file tests
The SDK validates content and structuredContent independently against the
outputSchema union, so a refactor that lets the two drift apart (e.g. a
stale image block in structuredContent while content carries the resource)
stays schema-valid and every test stays green. Mirror-equality assertions
on all four callTool cases close that gap (mutation-verified: a schema-valid
drifted structuredContent fails exactly these four assertions).
Also drops a vacuous .not.toBe('blob') assertion that could never add
signal, noting where the real "never blob" guard actually lives (the SDK
client's CallToolResultSchema rejection, upstream of any local assertion).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
@olaservo @domdomegg — could I get a review on this when you have a chance? 🙏 |
Member
|
Hi, just ran the npm release process for this change and its now live in https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem/v/2026.7.10 |
Contributor
Author
|
@olaservo thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
read_media_filereturns a content block withtype: "blob"for any file that isn'timage/*oraudio/*.blobis not a valid MCP content type — the content-block union istext | image | audio | resource_link | resource— so a strict client rejects the whole tool result on schema validation (surfaced asMCP error -32602 invalid_union; the reference TypeScript SDK raises a Zod parse error on the invalidcontent[0]). The result: for any non-image/non-audio file, this tool is unusable against a strict client.Closes #4029.
Fix
Map the MIME type to a valid MCP content block:
image/*→{ type: "image", data, mimeType }audio/*→{ type: "audio", data, mimeType }{ type: "resource", resource: { uri, mimeType, blob } }— the spec-correct carrier for arbitrary binary data — instead of the invalidtype: "blob".Alongside the handler change:
outputSchemais widened to the matching union (so the server-sidestructuredContentvalidation accepts both shapes);resource.uriis built withpathToFileURL()so spaces / non-ASCII / Windows paths are correctly percent-encoded;as unknown as CallToolResultcast is dropped, sotscenforces the returned shape;READMEare updated to note that non-media files are returned as an embedded resource.⚠ Behavior change (worth a release note)
For a file that is neither image nor audio,
read_media_filepreviously returned{ type: "blob", data, mimeType }and now returns{ type: "resource", resource: { uri, mimeType, blob } }— the base64 payload moves from.datato.resource.blob. Any client that read.data/.mimeTypeoff a blob-typed result for non-media files will need to read the embedded resource instead. Sincetype: "blob"was never a valid MCP content type (strict clients already rejected it), this is a correctness fix — but it may warrant aserver-filesystemversion bump / changelog entry.Alternative considered
If you'd rather keep
read_media_filestrictly image/audio, the alternative is to reject non-media files (dropblobfrom the enum andthrowfor unsupported types) — the approach in #4163. That's simpler but removes the ability to return arbitrary bytes. This PR takes the capability-preserving route; happy to switch to the reject approach if you prefer it.Tests
Adds a
read_media_file (issue #4029)suite tosrc/filesystem/__tests__/structured-content.test.ts. These are realclient.callTool()round-trips through the MCP SDK client, so a schema-invalid result fails the call:data;type: "resource"block (never"blob"), the base64 blob round-trips to the original bytes, and theuridecodes back to the file that was read;uriis percent-encoded (guardspathToFileURLvs a rawfile://string concatenation);tools/listadvertises the widenedoutputSchemaunion with both branches present (guards the JSON-Schema serialization path, which is separate from thecallToolvalidation the other cases exercise).callToolcase also assertsstructuredContentmirrorscontent— the SDK validates the two fields independently against the union, so mirror-equality is the only thing that catches them drifting apart (mutation-verified: a schema-valid driftedstructuredContentfails exactly these assertions).npm run build+npm testpass (152 tests).Related
pathToFileURLURI-encoding, drops theas unknown as CallToolResultcast, updates the README, and adds thetools/listserialization + uri-identity tests.openWorldHint: falseto all filesystem tools, touching this same file (including theread_media_fileblock); the two branches auto-merge cleanly in either order (verified locally).🤖 Generated with Claude Code