Skip to content

fix(filesystem): return valid MCP content type from read_media_file (#4029)#4479

Merged
olaservo merged 3 commits into
modelcontextprotocol:mainfrom
bryan-anthropic:fix/read-media-file-content-type
Jul 6, 2026
Merged

fix(filesystem): return valid MCP content type from read_media_file (#4029)#4479
olaservo merged 3 commits into
modelcontextprotocol:mainfrom
bryan-anthropic:fix/read-media-file-content-type

Conversation

@bryan-anthropic

@bryan-anthropic bryan-anthropic commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

read_media_file returns a content block with type: "blob" for any file that isn't image/* or audio/*. blob is not a valid MCP content type — the content-block union is text | image | audio | resource_link | resource — so a strict client rejects the whole tool result on schema validation (surfaced as MCP error -32602 invalid_union; the reference TypeScript SDK raises a Zod parse error on the invalid content[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 }
  • anything else → an embedded resource { type: "resource", resource: { uri, mimeType, blob } } — the spec-correct carrier for arbitrary binary data — instead of the invalid type: "blob".

Alongside the handler change:

  • the tool's outputSchema is widened to the matching union (so the server-side structuredContent validation accepts both shapes);
  • resource.uri is built with pathToFileURL() so spaces / non-ASCII / Windows paths are correctly percent-encoded;
  • the now-unnecessary as unknown as CallToolResult cast is dropped, so tsc enforces the returned shape;
  • the tool description and README are 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_file previously returned { type: "blob", data, mimeType } and now returns { type: "resource", resource: { uri, mimeType, blob } } — the base64 payload moves from .data to .resource.blob. Any client that read .data/.mimeType off a blob-typed result for non-media files will need to read the embedded resource instead. Since type: "blob" was never a valid MCP content type (strict clients already rejected it), this is a correctness fix — but it may warrant a server-filesystem version bump / changelog entry.

Alternative considered

If you'd rather keep read_media_file strictly image/audio, the alternative is to reject non-media files (drop blob from the enum and throw for 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 to src/filesystem/__tests__/structured-content.test.ts. These are real client.callTool() round-trips through the MCP SDK client, so a schema-invalid result fails the call:

  • image / audio return their content type, with a byte round-trip of the base64 data;
  • a non-media binary returns a type: "resource" block (never "blob"), the base64 blob round-trips to the original bytes, and the uri decodes back to the file that was read;
  • a spaced + non-ASCII filename asserts the uri is percent-encoded (guards pathToFileURL vs a raw file:// string concatenation);
  • tools/list advertises the widened outputSchema union with both branches present (guards the JSON-Schema serialization path, which is separate from the callTool validation the other cases exercise).
  • each callTool case also asserts structuredContent mirrors content — 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 drifted structuredContent fails exactly these assertions).

npm run build + npm test pass (152 tests).

Related

🤖 Generated with Claude Code

bryan-anthropic and others added 2 commits July 5, 2026 20:49
…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>
@bryan-anthropic

Copy link
Copy Markdown
Contributor Author

@olaservo @domdomegg — could I get a review on this when you have a chance? 🙏

@olaservo olaservo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@olaservo olaservo merged commit 97b70ee into modelcontextprotocol:main Jul 6, 2026
17 checks passed
@olaservo

Copy link
Copy Markdown
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

@bryan-anthropic

Copy link
Copy Markdown
Contributor Author

@olaservo thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filesystem: read_media_file returns invalid 'blob' content type, violating MCP spec

2 participants