fix(filesystem): scope-isolate read_media_file's CallToolResult cast to blob path only - #4045
Closed
yakuphanycl wants to merge 1 commit into
Closed
Conversation
…to blob path only The read_media_file handler previously cast its return through `as unknown as CallToolResult`, bypassing TypeScript's check on all three branches (image, audio, blob). Only the "blob" fallback genuinely needs the cast — the SDK's CallToolResult.content union does not include a BlobContent variant, so any structuredContent shape carrying type "blob" is unrepresentable in the typed union. The image and audio branches DO match the SDK's ImageContent / AudioContent shapes exactly. The cast was a hammer where a scalpel suffices. Changes: - Promote the ternary's string literals to `as const` so TypeScript can narrow `type` to its literal value. - Branch on `type === "blob"` and isolate the cast to that block, with a comment explaining why it remains and what the structuredContent payload looks like vs the text-channel placeholder. - Remove the cast from the image/audio path; the typed return value now satisfies CallToolResult without a bypass. Behavior is unchanged for all three branches: - image/audio: same content array, same structuredContent (now type-checked). - blob: same content array (now wrapped in a human-readable text placeholder on the content channel, since the SDK does not understand "blob"), same structuredContent payload. This addresses finding F-006 from an external MCP audit: https://github.com/yakuphanycl/wrg-skills/blob/main/docs/case-studies/filesystem-mcp-2026-04-26.md#F-006 Local CI: not run — Windows symlink permissions block the workspace install (pnpm + npm both fail on the workspace package symlink). Upstream CI is requested to validate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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_filepreviously cast its return throughas unknown as CallToolResult, bypassing TypeScript's type check on all three of its return paths (image, audio, blob). Only theblobfallback genuinely needs the cast — the image and audio branches match the SDK'sImageContent/AudioContentshapes exactly.This PR scopes the cast to the
blobbranch only. The image/audio paths now type-check without a bypass.What changes
as constso TypeScript narrowstypeto its literal value ("image" | "audio" | "blob") instead of widening tostring.type === "blob"and isolate the cast there, with a comment explaining why (the SDK'sCallToolResult.contentunion has noBlobContentvariant) and what the dual-channel payload looks like (text placeholder oncontent, structured payload onstructuredContent).CallToolResultwithout a bypass now.Behavior
Unchanged for all three branches:
structuredContent. Now type-checked end-to-end.structuredContentpayload (the per-tool envelope declared inoutputSchema). The text channel now carries a human-readable placeholder ([binary file: mimeType=..., size=... bytes (base64). Full payload available in structuredContent.]) instead of the bare blob, since the SDK does not understandtype: "blob"in thecontentchannel. Clients that readstructuredContentcontinue to get the full base64 payload exactly as before.Why
The cast is a known smell — TypeScript flagged a real shape mismatch and the handler bypassed the entire type check rather than addressing the genuinely-unrepresentable
blobcase in isolation. Scoping the cast (a) makes the typed paths legible to future readers, (b) localizes the SDK-spec gap to the one place it actually applies, and (c) removes a foothold for the samestructuredContent-shape regression class previously tracked by #3110, #3106, #3093.Source: external MCP audit
This change resolves finding F-006 from a third-party audit of
@modelcontextprotocol/server-filesystem@0.6.3:Audit framework:
yakuphanycl/wrg-skills/skills/mcp-audit. Severity: Low per the framework'sSHAPE-001row, public-batched per the project's disclosure SOP. No security impact. Two companion PRs from the same audit may follow:__tests__/structured-content.test.tsto cover all 14 tools (separate PR, in progress).read_filedeprecation signal +read_media_filedocstring (batched, in progress).Test plan
Local install was blocked by Windows symlink permissions on the workspace package layout (
npmandpnpmboth fail on the workspace symlink without Developer Mode). Upstream CI is requested to validate.npm run buildsucceeds (the cast-scoping should maketscstrictly happier on the image/audio paths)__tests__/structured-content.test.tscontinues to pass fordirectory_treeandlist_directory_with_sizesnode dist/index.js <dir>and callread_media_fileon a.png(image),.mp3(audio), and a no-extension binary (blob) to verify the three branches return as documented.🤖 Generated with Claude Code