Skip to content

feat: support local file paths in media tools - #3

Merged
sommio merged 1 commit into
developfrom
feat/local-file-input
Jul 29, 2026
Merged

feat: support local file paths in media tools#3
sommio merged 1 commit into
developfrom
feat/local-file-input

Conversation

@sommio

@sommio sommio commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

The 6 content tools (analyze_video, analyze_image, summarize_video, extract_video_text, video_qa, compare_video_frames) now accept a local file path in addition to a public URL. Local files are read and sent inline as base64 data URLs via the OpenAI-compatible endpoint — verified live (14MB video / ~18MB body, HTTP 200 on qwen3.7-plus).

This removes the previous requirement to host every media file at a public URL before the tools could touch it.

Changes

  • src/media.ts (new): isRemoteUrl, isLocalPath, mimeFromExt, toDataUrl, resolveMedia. Local path → base64 data URL; remote URL → passthrough.
  • src/server.ts: mediaCall resolves input via resolveMedia before analyze; the 6 tool schemas relax z.string().url() to accept a URL or local path (field name + string type unchanged — backward compatible). Tool descriptions and list_capabilities notes updated.
  • src/bailian.ts: untouched — it already passes the url string transparently into the content block, so data URLs work as-is.
  • Tests: test/media.test.ts (new, 32 cases) + test/tools.test.ts (+5 msw e2e) + test/live.test.ts (rewritten to exercise resolveMedia end-to-end against the real API).
  • AGENTS.md: fragile assumption feat: support local file paths in media tools #3 updated — local video ≤25MB can be base64 (the prior “>10MB cannot” claim was disproven live).

Security

toDataUrl guards against unrestricted local-file exfiltration to DashScope (a prompt-injected/malicious MCP client could otherwise pass image_url="/home/.../.env" or ~/.ssh/id_rsa and ship the bytes to a third-party endpoint):

  • 25MB size guardrail (verified-safe ceiling + headroom).
  • Extension whitelist — rejects .env, id_rsa, /etc/passwd, .bashrc, etc. before reading.
  • Magic-byte signature check — rejects renamed non-media files (e.g. a text secret renamed to secret.png). Also defeats symlink-to-secret attacks, so no lstat guard is needed (which would have rejected legitimate symlinked media).

Surfaced by an adversarial review workflow; also fixed a TOCTOU in the size guard (re-check bytes read) and wrapped readFile failures into the Cannot read local file contract.

Verification

  • npm run typecheck && lint && format:check && test && build — all green.
  • Coverage: 98.84% statements / 95.04% branches (threshold 85/75).
  • LIVE=1 end-to-end against real DashScope: bundled PNG (5.8s), local 图1.jpg (19s), local 14MB video.mp4 (79s) — all return real answers through the full resolveMediaanalyze path.

Six content tools now accept a local file path in addition to a
public URL; local files are sent inline as base64 data URLs (verified
live: 14MB video / ~18MB body, HTTP 200).

toDataUrl blocks unrestricted file exfiltration to DashScope via a
25MB cap, extension whitelist, and magic-byte signature check — rejects
non-media files (.env, id_rsa, renamed secrets) before encoding.
bailian.ts unchanged; it passes the url string through as-is.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for providing local image and video files directly to media analysis tools.
    • Local files are validated and sent as inline base64 media, with a 25MB size limit.
    • Public HTTP/HTTPS media URLs continue to work unchanged.
    • Added validation for file types, extensions, and content signatures.
  • Bug Fixes

    • Improved handling and error reporting for missing, invalid, oversized, or unsupported local media files.
  • Documentation

    • Updated capability information and guidance to describe local-file support and size limits.

Walkthrough

Local image and video paths can now be validated and converted to base64 data URLs. MCP tools accept either public HTTP(S) URLs or local paths, while remote URLs pass through unchanged. Unit, tool, and live tests cover the new behavior and limits.

Changes

Local media support

Layer / File(s) Summary
Media classification and validated encoding
src/media.ts, test/media.test.ts
Adds URL/path detection, MIME mapping, a 25MB guardrail, magic-byte validation, safe file reads, and base64 data-URL conversion with comprehensive tests.
Tool validation and media resolution
src/server.ts, test/tools.test.ts
Updates MCP schemas and request handling to accept local paths, resolve media before analysis, document the behavior, and test local, remote, and invalid inputs.
Live smoke-test migration
test/live.test.ts
Routes local image and video fixtures through resolveMedia and verifies resulting data URLs.
Fragile-assumption documentation
AGENTS.md
Documents the 25MB limit and extension plus magic-byte validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCP Client
  participant MCP Server
  participant resolveMedia
  participant Local Filesystem
  participant Analyze Backend
  MCP Client->>MCP Server: Submit URL or local path
  MCP Server->>resolveMedia: Resolve media input
  resolveMedia->>Local Filesystem: Read and validate local file
  Local Filesystem-->>resolveMedia: Validated file bytes
  resolveMedia-->>MCP Server: Data URL or unchanged remote URL
  MCP Server->>Analyze Backend: Analyze resolved media
Loading

Possibly related PRs

  • sommio/qwen-omni-mcp#1: Extends existing media tool plumbing and URL validation to support local paths through resolveMedia.

Poem

A bunny found a file in the burrow bright,
Checked its bytes by morning light.
Small enough? Encode and send,
Remote links pass straight to the end.
Safe media hops along—
Hop, hop, validated strong!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding local file path support to media tools.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the new media path support and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/local-file-input
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/local-file-input

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server.ts`:
- Around line 33-37: Update the mediaInput schema predicate to accept only valid
remote HTTP(S) URLs or local paths, explicitly rejecting non-HTTP(S) schemes
such as ftp:// and file:// while preserving the existing validation message and
isLocalPath classifier.

In `@test/live.test.ts`:
- Line 12: Update the live-test fixture path initialization around
BUNDLED_SAMPLE so it does not use import.meta.dirname on unsupported Node
versions, or raise the package.json Node engine floor to the first version
supporting that API. Preserve live-test loading and fixture resolution across
the declared runtime range.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: be9ede19-2154-4298-87e0-ae38bd149cc3

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac5e6f and e62d025.

📒 Files selected for processing (6)
  • AGENTS.md
  • src/media.ts
  • src/server.ts
  • test/live.test.ts
  • test/media.test.ts
  • test/tools.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,mjs,json,md,env}

📄 CodeRabbit inference engine (AGENTS.md)

Never commit or hardcode secrets, API keys, tokens, or .env files. Read the DashScope key from DASHSCOPE_API_KEY via src/config.ts; use dummy values such as sk-test in fixtures.

Files:

  • AGENTS.md
  • test/live.test.ts
  • src/media.ts
  • test/media.test.ts
  • test/tools.test.ts
  • src/server.ts
**/*

📄 CodeRabbit inference engine (AGENTS.md)

Delete files with trash, never rm.

Files:

  • AGENTS.md
  • test/live.test.ts
  • src/media.ts
  • test/media.test.ts
  • test/tools.test.ts
  • src/server.ts
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

test/**/*.{ts,tsx}: Unit and mocked end-to-end tests must use MSW to mock fetch and must not make real API calls.
Every new tool or branch of logic must have a test, and test coverage must remain at least 85%.

Files:

  • test/live.test.ts
  • test/media.test.ts
  • test/tools.test.ts
test/live.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Live tests may call the real API only when LIVE=1 with a real DASHSCOPE_API_KEY; they must remain excluded from default npm test.

Files:

  • test/live.test.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Use TypeScript strict typing in src/: do not use any, @ts-ignore, or non-null assertions; prefer narrow types and unknown when parsing external JSON.
Keep the DashScope payload builder buildPayload injectable, and make content-block shape changes for video_url or image_url in one place.
Preserve the eight MCP tool names and argument schemas in src/server.ts: analyze_video, analyze_image, summarize_video, extract_video_text, video_qa, compare_video_frames, check_endpoint_status, and list_capabilities. Add tools rather than silently renaming or changing existing ones.
Ensure check_endpoint_status redacts the API key using redactKey; its no-leak test must continue passing.
Use the Bailian DashScope OpenAI-compatible endpoint ${DASHSCOPE_BASE_URL}/chat/completions, defaulting to https://dashscope.aliyuncs.com/compatible-mode/v1, with model qwen3.7-plus.
Do not switch multimodal tools to the Anthropic-compatible /apps/anthropic endpoint, because it does not support video input.
Do not add client-side video frame extraction; video frame sampling is server-side at 0.5 seconds per frame in OpenAI-compatible mode.
Keep the OpenAI-compatible video_url assumption and fallback logic centralized in contentBlock() in src/bailian.ts; use the documented fallback only if the endpoint rejects it.
Use the exact model ID qwen3.7-plus, verifying it against the Bailian model list if a model-not-found error occurs.
Enforce the 25MB local-media guardrail, validate files by extension and magic-byte signature, and encode eligible local files as base64 data URLs; larger files must use a public URL.

Files:

  • src/media.ts
  • src/server.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T04:30:37.233Z
Learning: If a secret is accidentally staged, unstage it, rotate the key immediately, and notify the maintainer.
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T04:30:37.233Z
Learning: Never use `git commit --no-verify` or `git push --no-verify`; fix hook failures instead of bypassing them.
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T04:30:37.233Z
Learning: After cloning, run `npm install` to install Husky hooks and verify `git config core.hooksPath` is `.husky`.
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T04:30:37.233Z
Learning: Before push, all quality gates must pass: `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm test`, and `npm run build`.
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T04:30:37.233Z
Learning: Do not add a new runtime, language, or heavy dependency without explicit maintainer approval.
🔇 Additional comments (22)
AGENTS.md (1)

68-68: LGTM!

src/media.ts (6)

6-16: LGTM!


18-45: LGTM!


47-56: LGTM!


58-87: LGTM!


89-126: LGTM!


128-138: LGTM!

test/media.test.ts (6)

1-23: LGTM!


25-78: LGTM!


80-148: LGTM!


150-183: LGTM!


198-218: LGTM!


185-195: 📐 Maintainability & Code Quality

No change needed.

context.skip()/t.skip() is the supported runtime skip API for conditional test skipping in Vitest 2, and the trailing return does not prevent the skip behavior.

src/server.ts (3)

64-65: LGTM!


82-84: LGTM!

Also applies to: 104-106, 128-128, 151-151, 162-162, 175-175


232-233: LGTM!

test/tools.test.ts (3)

1-6: LGTM!

Also applies to: 201-210


212-265: LGTM!


267-293: LGTM!

test/live.test.ts (3)

8-11: LGTM!

Also applies to: 14-18


21-24: LGTM!

Also applies to: 34-39


51-66: LGTM!

Comment thread src/server.ts
Comment on lines +33 to +37
const mediaInput = (description: string) =>
z
.string()
.refine((v) => isRemoteUrl(v) || isLocalPath(v), "Must be a public URL or a local file path")
.describe(description);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The refine predicate is tautological — it only rejects empty strings.

isLocalPath(v) is v.length > 0 && !isRemoteUrl(v), so isRemoteUrl(v) || isLocalPath(v) reduces to v.length > 0. Non-file schemes like ftp://h/x.mp4 or file:///x.mp4 pass validation and later fail in toDataUrl as Cannot read local file: file:///x.mp4, which reads as a filesystem error rather than a rejected input.

Consider rejecting non-http(s) URL schemes explicitly so the message matches the actual contract.

♻️ Tighten the predicate
 const mediaInput = (description: string) =>
   z
     .string()
-    .refine((v) => isRemoteUrl(v) || isLocalPath(v), "Must be a public URL or a local file path")
+    .refine(
+      (v) => isRemoteUrl(v) || (isLocalPath(v) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(v)),
+      "Must be a public http(s) URL or a local file path",
+    )
     .describe(description);

This keeps isLocalPath as the shared classifier while making the scheme rule explicit at the tool boundary.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mediaInput = (description: string) =>
z
.string()
.refine((v) => isRemoteUrl(v) || isLocalPath(v), "Must be a public URL or a local file path")
.describe(description);
const mediaInput = (description: string) =>
z
.string()
.refine(
(v) => isRemoteUrl(v) || (isLocalPath(v) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(v)),
"Must be a public http(s) URL or a local file path",
)
.describe(description);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 33 - 37, Update the mediaInput schema predicate
to accept only valid remote HTTP(S) URLs or local paths, explicitly rejecting
non-HTTP(S) schemes such as ftp:// and file:// while preserving the existing
validation message and isLocalPath classifier.

Comment thread test/live.test.ts
const LOCAL_ASSET_DIR =
process.env.QWEN_LIVE_ASSET_DIR ??
"/home/sommio/Downloads/2026-07-08-01-test10_V1_fix1/cat_dialogue_10/cat_dialogue_000001";
const BUNDLED_SAMPLE = join(import.meta.dirname, "fixtures", "sample.png");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check declared Node version floor across manifests and CI.
fd -H -t f 'package.json' -E node_modules --exec jq -r '{path: input_filename, engines: .engines, volta: .volta}' {} \;
fd -H -t f '.nvmrc|.node-version|.tool-versions' -E node_modules --exec sh -c 'echo "--- $1"; cat "$1"' _ {} \;
rg -n --iglob '.github/workflows/*.y*ml' -C2 'node-version|setup-node'
# Other usages of import.meta.dirname / import.meta.filename
rg -n 'import\.meta\.(dirname|filename)' -g '!node_modules'

Repository: sommio/qwen-omni-mcp

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -u

echo "--- package and lockfiles"
for f in package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lockb bun.lock; do
  if [ -e "$f" ]; then
    echo "### $f"
    case "$f" in
      package.json) jq '{engines, type}' "$f";;
      *) head -80 "$f";;
    esac
  fi
done

echo "--- Node files"
fd -H -t f '.nvmrc|.node-version|.tool-versions|package.json' -E node_modules

echo "--- CI node-version references"
rg -n --iglob '.github/workflows/*.y*ml' -C2 'node-version|setup-node' || true

echo "--- import.meta.dirname/filename usages"
rg -n 'import\.meta\.(dirname|filename)' -g '!node_modules' || true

Repository: sommio/qwen-omni-mcp

Length of output: 2961


🌐 Web query:

Node.js CHANGELOG import.meta.dirname added 20.11.0

💡 Result:

The import.meta.dirname property (along with import.meta.filename) was added to Node.js in version 20.11.0 [1][2][3]. This addition was tracked in the Node.js CHANGELOG under the esm module section as a semver-minor change [1][4]. These properties serve as the ESM equivalents to the CommonJS dirname and filename variables, respectively [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -u

echo "--- lockfile `@types/node` and vitest/test runner entries"
python3 - <<'PY'
import json
from pathlib import Path
p = Path("package-lock.json")
data = json.loads(p.read_text())
packages = data.get("packages", {})
for key in [
    "node_modules/@types/node",
    "node_modules/vitest",
    "node_modules/@vitest/runner",
    "node_modules/@types/estree",
    "node_modules/rollup",
]:
    val = packages.get(key)
    if val:
        print("###", key)
        print(json.dumps({k: val.get(k) for k in ("name","version","engines","dependencies","devDependencies") if k in val}, indent=2))
PY

echo "--- lockfile `@types/node` version"
python3 - <<'PY'
import json
print(json.load(open("package-lock.json"))["packages"]["node_modules/@types/node"]["version"])
PY

echo "--- package scripts"
jq '.scripts' package.json

Repository: sommio/qwen-omni-mcp

Length of output: 1891


Narrow the Node engine floor to where import.meta.dirname is available.

import.meta.dirname is only available from Node 20.11+/21.2+, but package.json currently declares >=20, so Node 20.9/20.10 can throw at live-test module load instead of skipping the tests. Raise the runtime floor to cover it or avoid this API in test/live.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/live.test.ts` at line 12, Update the live-test fixture path
initialization around BUNDLED_SAMPLE so it does not use import.meta.dirname on
unsupported Node versions, or raise the package.json Node engine floor to the
first version supporting that API. Preserve live-test loading and fixture
resolution across the declared runtime range.

@sommio
sommio merged commit 7996bc9 into develop Jul 29, 2026
4 checks passed
@sommio
sommio deleted the feat/local-file-input branch July 29, 2026 04:55
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.

1 participant