Skip to content

feat(mcp): MediaSanitizer decode/validate/attach + vision-tier routing for MCP image passthrough#6331

Merged
bug-ops merged 2 commits into
mainfrom
feat/issue-6240/mcp-media-sanitizer
Jul 16, 2026
Merged

feat(mcp): MediaSanitizer decode/validate/attach + vision-tier routing for MCP image passthrough#6331
bug-ops merged 2 commits into
mainfrom
feat/issue-6240/mcp-media-sanitizer

Conversation

@bug-ops

@bug-ops bug-ops commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the P2 phase of spec-072 (multimodal MCP passthrough, parent #6229). An MCP server can now return an image in a tool result and have it attached as a native MessagePart::Image sibling part, visible to a vision-capable provider, instead of the previous text-only [image: mime, N bytes] placeholder.

  • New zeph-sanitizer::MediaSanitizer: magic-byte sniff vs. declared MIME, format allowlist, byte-size cap, a header-only dimension pre-check (image::ImageReader::into_dimensions) before a spawn_blocking full decode, with the same dimension/pixel caps re-enforced on the decoded image — the header check rejects an oversized image without paying for the full decode's memory allocation (decompression-bomb defense-in-depth).
  • New [[mcp.servers]].media_passthrough per-server opt-in flag (default false), always hard-blocked when trust_level = "sandboxed" regardless of the flag. New [mcp.media] global caps (5 MiB / 8192px / ~64MP / 4-per-result / 8-per-turn / jpeg,png,gif,webp).
  • zeph-mcp::McpToolExecutor gates media decode on the opt-in flag and trust level, logs every accept/reject decision via the existing tool audit path.
  • ToolResultClassification.media populated only on tool success; process_one_tool_result emits sibling MessagePart::Image parts respecting a max_images_per_turn running cap, never producing a runtime 400/422.
  • Both TriageRouter and RouterProvider (Cascade/Bandit/Ema/Thompson strategies) aggregate vision capability across their configured tiers and strip Image parts before dispatch when no tier can be guaranteed vision-capable — closes a gap found during review where the non-triage router strategies silently dropped images without a safety net.
  • Static system-prompt caveat marks tool-sourced images as untrusted data whenever any server has media_passthrough enabled, added once per session before the cache-stability marker.
  • Debug-dump base64 redaction gap for Gemini's camelCase payload shape was independently closed by debug_dump writes raw MessagePart::Image base64 bytes to disk #6306/fix(core): redact image bytes from debug dumps by default #6314 during this branch's rebase; no duplicate logic added here.

Review chain

Reduced new-feature chain (architect/pre-critique skipped — issue carried exact file-level scope from specs/072-multimodal-mcp-passthrough/plan.md): developer → tester/perf/security/impl-critic (parallel) → code reviewer, with one fix-review round for two significant impl-critic findings (RouterProvider vision-blindness risk of a real 400/422 under Cascade/Bandit/Ema/Thompson; a Gemini debug-dump redaction gap, resolved upstream) and one further trivial comment-fix round. Final reviewer verdict: approved.

Test plan

  • cargo +nightly fmt --check
  • cargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,testing" -- -D warnings
  • cargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins — 13844 passed, 0 failed
  • RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --features "desktop,ide,server,chat,pdf,scheduler"
  • cargo test --doc --workspace --features "desktop,ide,server,chat,pdf,scheduler"
  • gitleaks protect --staged --no-banner --redact — no leaks
  • New/updated playbook: .local/testing/playbooks/mcp-media-passthrough.md, 5 new coverage-status rows (all currently Untested pending a live session)
  • Mandatory LLM Serialization Gate (spec-072 AC-6, live cascade + MCP-image session): attempted but environment-blocked — Claude/OpenAI hit real billing/quota exhaustion (independently reproduced via bare curl, matches known team precedent for 2026-07), local Ollama hit a suspected unrelated pre-existing cached_prompt_tokens bug plus a hardware/idle-unload timeout on the 7B model. The exact routing logic this gate exists to validate is covered by 9+ non-tautological mocked-provider unit tests in triage.rs/router/tests.rs. Recommend re-attempting once cloud credits are replenished; will file the cached_prompt_tokens anomaly as a separate follow-up issue.

Closes #6240

@github-actions github-actions Bot added enhancement New feature or request size/XL Extra large PR (500+ lines) documentation Improvements or additions to documentation llm zeph-llm crate (Ollama, Claude) rust Rust code changes core zeph-core crate dependencies Dependency updates config Configuration file changes and removed size/XL Extra large PR (500+ lines) labels Jul 16, 2026
@bug-ops
bug-ops enabled auto-merge (squash) July 16, 2026 18:40
@bug-ops
bug-ops force-pushed the feat/issue-6240/mcp-media-sanitizer branch from c1ba842 to bdac0f9 Compare July 16, 2026 18:40
@github-actions github-actions Bot added the size/XL Extra large PR (500+ lines) label Jul 16, 2026
bug-ops added 2 commits July 16, 2026 20:58
…uting for MCP image passthrough

Adds the P2 phase of spec-072 (multimodal MCP passthrough): an MCP server
can now return an image in a tool result and have it attached as a native
MessagePart::Image sibling, visible to a vision-capable provider, instead
of the previous text-only placeholder.

- zeph-sanitizer::MediaSanitizer: magic-byte sniff vs declared MIME, format
  allowlist, byte-size cap, header-only dimension pre-check before a
  spawn_blocking full decode, with dimension/pixel caps re-enforced on the
  decoded image (decompression-bomb defense-in-depth).
- New per-server media_passthrough opt-in (default false, hard-blocked for
  Sandboxed trust regardless of the flag) and [mcp.media] global caps.
- McpToolExecutor gates media decode on the opt-in flag and trust level,
  audit-logs every accept/reject decision.
- ToolResultClassification.media populated only on tool success;
  process_one_tool_result emits sibling Image parts respecting a
  max_images_per_turn cap, never producing a runtime 400/422.
- TriageRouter and RouterProvider (Cascade/Bandit/Ema/Thompson) both
  escalate to a vision-capable tier when a pending request carries media,
  and strip Image parts before dispatch when no tier can be guaranteed
  vision-capable.
- Static system-prompt caveat marks tool-sourced images as untrusted data
  whenever any server has media_passthrough enabled.

Closes #6240
New McpMediaConfig/media_passthrough fields grew Config's size enough to
push handle_skill_command's future past clippy's large_futures threshold
(16432 bytes) under the CI feature set. Box the three test call sites,
mirroring the existing install_test_skill boxing in the same module.
@bug-ops
bug-ops force-pushed the feat/issue-6240/mcp-media-sanitizer branch from 2e10e23 to d4755d5 Compare July 16, 2026 19:00
@bug-ops
bug-ops merged commit bce63ae into main Jul 16, 2026
79 of 81 checks passed
@bug-ops
bug-ops deleted the feat/issue-6240/mcp-media-sanitizer branch July 16, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Configuration file changes core zeph-core crate dependencies Dependency updates documentation Improvements or additions to documentation enhancement New feature or request llm zeph-llm crate (Ollama, Claude) rust Rust code changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement: P2 MediaSanitizer decode/validate/attach + vision-tier routing for MCP image passthrough

1 participant