Qwen-MM-PluginsのcoreをClaude CodeとCodexに入れ、APIキーなしでPDF・動画・STLを読ませた
目次
今月のQwenネタを探していたら、QwenLMが Qwen-MM-Plugins というのを出していた。
Claude CodeとかCodexみたいなコーディングエージェントに、画像・動画・文書を読む機能をスキルとMCPサーバーの組で足すやつ。
機能(capability)は8つあって、そのうち core は既定のnative-image modeならAPIキーなしで、ローカルのファイルを読んだり可視化したり、画像を切り出したり注釈したりする。
Qwenの名前は付いているが、既定の QWEN_MM_NATIVE_MODE=1 だと core 自身はQwenのモデルを呼ばない。
呼ぶ側のモデル(Claude CodeならClaude、CodexならGPT)へ画像ブロックを返すレンダラー集、というのが実態。
手元のM4 Mac miniはメモリ16GBでQwen3.8-27Bは載らないし、api 側はDashScopeのキーがいる。
APIキーなしで動く core だけで試すことにして、Claude CodeとCodexの両方に入れて、画像・PDF・SVG・CSV・STL・動画を読ませてみた。
検証環境
| 項目 | 内容 |
|---|---|
| 機材 | M4 Mac mini、メモリ16GB |
| Claude Code | 2.1.239 |
| Codex CLI | 0.149.0(モデル gpt-5.6) |
| uv | 0.9.21(uvx でMCPサーバーを起動する) |
| Qwen-MM-Plugins | distribution_version 1.0.7、core 1.0.4(2026年8月21日時点のmain) |
| 対象 | core のみ。api はDashScopeのキーが前提で、手元のQwenアンバサダー枠にはVL/Omniのモデルが入っていない |
| ffmpeg | 入ってなかったので brew install ffmpeg で9.0.1を入れた |
| 素材 | Anima-2.9Bのサンプル画像、コミックマーケット108の会場配置図PDF(4ページ)、過去記事のラグランジュ点SVG、自作の8行CSV、自作の四角錐STL、ffmpegで作った12秒の動画 |
Qwen-MM-Pluginsの構成
READMEの表をそのまま写すとこんな感じ。
機能は8つで、それぞれ独立したスキルと、必要ならMCPサーバーが付く。edu-agent だけはスキルのみ。
| 機能 | 用途 | 主な要件 |
|---|---|---|
core | 画像・動画を読む、文書・コード・データ・3Dを可視化する | APIキー不要。動画・音声にffmpeg |
api | Qwen VL/Omniで画像理解、OCR、grounding(物体の位置特定)、ASR、セグメンテーション | DashScope |
search | Web検索、ページ抽出、画像逆検索 | Serper / Exa / Tavily のキー |
video-memory | 長時間動画への質問応答用に階層メモリを作る | DashScope、ffmpeg |
video-edit | 画像・動画・音声の生成と編集 | DashScope、ffmpeg、Node/Chromium |
blender | Blenderでモデリング・レンダリング | Blender |
freecad | パラメトリックCAD、STEP/STL、FEM | FreeCAD |
edu-agent | 中国語の数学・理科の解説動画を作る | スキルのみ。Node/Chromium、ffmpeg。ナレーション付き動画にはDashScope |
core のMCPサーバーが持つツールは7つ。
read_image、read_video、media_info、visualize が読む側、crop、draw_bbox、save_view が画像ファイルを書き出す側。
visualize は拡張子で振り分けていて、PDFは pypdfium2、SVGは resvg、CSV/XLSXは pandas+matplotlib、3Dは Blender → pyrender → matplotlib の順で試す。
返り値はMCPの画像ブロックで、既定の QWEN_MM_NATIVE_MODE=1 ならモデルへ画像をそのまま返す。
呼ぶ側がテキスト専用モデルのときは QWEN_MM_NATIVE_MODE=0 にすると、画像ブロックの代わりにDashScope経由のVLモデル(既定 qwen3.7-plus)が付けたキャプションが返ってくる。こっちはAPIキーがいる。
Claude Codeへの導入
READMEの案内は curl | bash の対話インストーラだが、cookbookにClaude Codeのネイティブなプラグインコマンドが書いてあったのでそっちで。
claude plugin marketplace add https://github.com/QwenLM/Qwen-MM-Plugins.git
claude plugin install qwen-mm-plugins-core@qwen-mm-plugins
マーケットプレイスの追加が35秒、プラグインの導入が4.6秒。
書き込まれたのは ~/.claude/settings.json の enabledPlugins と extraKnownMarketplaces、~/.claude/plugins/marketplaces/qwen-mm-plugins/(リポジトリのclone、70MB)、~/.claude/plugins/cache/qwen-mm-plugins/qwen-mm-plugins-core/1.0.4/(SKILL.mdとMCP定義で188KB)の3か所。
claude plugin details で見ると常駐コストは約91トークン、スキルが使われたときに約1.8kトークン。
MCPサーバー本体はこの時点ではまだ無い。
MCP定義の uvx --from "qwen-mm-plugins[core] @ git+https://github.com/QwenLM/Qwen-MM-Plugins.git@qwen-mm-plugins-core-v1.0.4" qwen-mm-plugins-core が初回起動時にPythonパッケージを取りに行く。
--check-system 付きで先に起動したら、75パッケージの取得と展開で126秒かかった。
uvのキャッシュに入ったのは約1GBで、playwright(132MB)、z3-solver(102MB)、scipy、pandas、pyogrio、shapely、openai あたり。2回目以降の起動は0.9秒。
--check-system はシステム側で足りないものを並べてくれる。
✗ read_video / media_info (video & audio) [core]
needs: ffmpeg or ffprobe
✗ visualize: 3D best-quality render (Blender; else falls back to matplotlib) [viz]
needs: blender
✗ visualize: Office / DrawIO (LibreOffice) [viz]
needs: libreoffice or soffice
✗ visualize: LaTeX (.tex) [system]
needs: pdflatex
✗ visualize: HTML screenshot (Playwright browser) [viz]
needs: playwright chromium
✗ DASHSCOPE_API_KEY not set — run `qwen-mm-plugins-core --setup`
このMac miniにはffmpegが入ってなかったので brew install ffmpeg した。
Blender、LibreOffice、TeX、Chromiumは入れてないので、Office文書とHTMLとLaTeXの可視化は今回はなし。
最後の行のDashScopeキーは、今回使った既定の QWEN_MM_NATIVE_MODE=1 ではいらないが、チェックには出る。
ツール名に plugin_ が付く
Claude Codeを新しいセッションで起動すると、MCPサーバーは plugin:qwen-mm-plugins-core:qwen-mm-plugins-core という名前で connected になる。
ツールは mcp__plugin_qwen-mm-plugins-core_qwen-mm-plugins-core__read_image みたいな名前で並ぶ。
cookbookが許可設定の例として挙げている mcp__qwen-mm-plugins-core__* と違う。
あっちは claude mcp add で直接登録した場合の名前で、プラグインとして入れるとサーバー名の前に plugin_ とプラグイン名が挟まる。
最初これがわからなかったので、非対話の claude -p で --allowedTools にcookbook通りの名前を書いて画像・PDF・SVGの3本を流した。
3本とも、モデルがツールを呼んだ瞬間に「権限を要求したがまだ付与されていない」と言われ、「対話セッションで許可してください」と返してきて終わってしまった。
--allowedTools を mcp__plugin_qwen-mm-plugins-core_qwen-mm-plugins-core__read_image に書き直したら通った。
もう1つ、Claude Codeはこのサーバーのツールを遅延読み込み(deferred)扱いにしていて、モデルは呼ぶ前に毎回 ToolSearch でスキーマを読み込んでいた。
プロンプト1回で ToolSearch → ツール呼び出し → 回答まで3〜5ターン、20〜45秒。
画像・PDF・SVG
ここからは --disallowedTools Read,Bash を付けて、組み込みのReadで画像を見る方法を使えないようにした上で、core のツールだけで読ませてみた。
画像は Anima-2.9Bのモデルカードに置いてあるサンプルの1枚(preview_2.9b_00001_.png、夜景のバーで黒いドレスの4人が座っている絵)を、900×675に縮小したものを使った。
read_image は 675x900 → 672x896 にリサイズしたJPEGを返してきた。
Claudeは4人の人物の髪色(赤、ピンク、ワインレッド、青)と黒いドレスの種類、光輪や角まで拾った。
budget は small / normal / large の3段階で、モデルが画像を格子に区切る単位(パッチグリッド)に合わせた画素数に丸める。
PDFは visualize に pages: "1-2" を指定すると、[PDF Start]、Total pages: 4 | Showing pages: 1-2、ページごとの [Page 1 View] 1216x1728 (HxW) と画像ブロック、[PDF End] の順で返ってくる。
コミックマーケット108の会場配置図で、Claudeは開催日、ホール名、島のカタカナ表記、「2日目はありません」の注記、2ページ目の全体位置図まで読み取った。

SVGは最初に read_image を呼んで cannot identify image file でこけた。
PILがSVGを開けないためで、visualize の方は拡張子で resvg に振り分ける。
Claudeはこのエラーを見て自分で visualize に切り替え、[SVG View] 640x1184 (HxW) の画像から地球・月・L1〜L5の配置と日本語ラベル、下の注記まで読み取った。
SVGのソースをテキストで読んでも答えは出そうだが、指示で禁じていたので、絵として認識できていた。

CSVとSTL
CSVは visualize がMarkdownの表テキストと、matplotlibで描いた表の画像の2つを返す。
SKILL.mdには「Text table + chart image」と書いてあるが、返ってきた画像は表をそのまま描画したものだった。
ソースの renderers/data.py にも表を描く関数しかない。
Claudeは表テキストから数値の傾向をつかんで、画像については「グラフではなく表の画像だった」と報告してきた。

STLはBlenderが無いので pyrender にフォールバックして、Perspective(仰角30度、方位45度)、Front(0度、0度)、Top(90度、0度)の3視点を1216×1216で返す。
タイトルは 5 vertices, 6 faces と頂点・面の数になっていた。

自作のSTLはZ軸を上にした四角錐だが、Frontの絵は頂点側から見下ろした正方形、Topの絵は横から見た三角形。
model3d.py のカメラ配置は y = distance * sin(elev) で、仰角をY軸に取るY-up(Y軸が上)前提になっている。
なのでZ-up(Z軸が上)で作ったSTLは Front と Top が入れ替わって見える。
Claudeもこの絵を見て「FrontとTopが直感と入れ替わっている」と言った上で、5頂点6面から四角錐と判定した。
視点ラベルの「pyramid.stl — Perspective」は、emダッシュのところが豆腐(欠けた文字の四角)で出た。描画に使うフォントにemダッシュのグリフが無いらしい。
動画
ffmpegで、前半6秒が testsrc2 の動くテストパターン、後半6秒が smptebars の静止カラーバー、音声は440Hzと880Hzの正弦波、という12秒の動画を作った。
media_info はffprobeでコンテナ、長さ、ビットレート、ストリームごとのコーデック・解像度・fps・音声のサンプルレートとチャンネル数を返す。
read_video は fps=1 で12フレームを672×384のJPEGとして返してきて、各フレームの前には <0.0s> みたいなタイムスタンプが書いてあった。

Claudeは1fpsの12枚で切り替わりが5.4〜6.5秒の間だと当たりを付けて、自分で start_time: 5.4, end_time: 6.6, fps: 10 で11フレームを読み直し、6.0秒でハードカットと答えた。
SKILL.mdに「1fpsで流し読みしてから興味のある区間を2fps・largeで詳しく」と書いてあって、モデルはそのとおりにやっていた。
media_info → read_video → read_video → 回答で39秒。
Codexでの導入と承認ポリシー
Codex CLIも0.149.0にはプラグインのマーケットプレイスがあって、同じリポジトリがそのまま使える。
codex plugin marketplace add https://github.com/QwenLM/Qwen-MM-Plugins.git
codex plugin add qwen-mm-plugins-core@qwen-mm-plugins
マーケットプレイスの追加が32秒、導入が2.3秒。
~/.codex/config.toml に [marketplaces.qwen-mm-plugins] と [plugins."qwen-mm-plugins-core@qwen-mm-plugins"] が追記されて、codex plugin list では8つの機能が並んで core だけ installed, enabled になった。
codex exec で、Claude Codeのときとは別のサンプル(preview_2.9b_00003_.png、紅葉の木の下に座る金髪の女性の絵。こちらも675×900に縮小)を読ませると、gpt-5.6はまず sed でSKILL.mdを読んでから read_image を呼んだ。
ただし既定の codex exec だとここで MCP tool call requires approval, but approval policy is never と出てこけた。
CodexはMCPツールの呼び出しを承認対象にしていて、execの既定ポリシーは承認を求めずに拒否する。
--approve-for-me か --dangerously-bypass-approvals-and-sandbox を付けると通った。
どっちも read_image の返り値は text と image の2ブロックで、gpt-5.6は銀白の髪、白・黒・金のチャイナドレス風の衣装、夕陽と紅葉の背景を答えた。
piのような手動設定のハーネス
インストーラが対応するのはClaude Code、CodeBuddy、Codex、Qoder、OpenClaw、Qwen Code、Gemini CLIで、pi、opencode、Hermes Agent、DeepSeek Harness、QwenPawは手動設定。
piはスキルのディレクトリをコピーした上で、MCPは pi install npm:pi-mcp-adapter のコミュニティ製アダプタを入れて、~/.config/mcp/mcp.json にサーバーを書く手順になっている。
そういえば以前、別のPCでpiにQwen3.8を繋いだとき、画像ファイルを指して「読め」と言っても読めなかった。
一方でサイトを作らせたときは画面のキャプチャを見ていたので、挙動が謎だった。
Qwen3.8-27Bは画像入力を持つので、モデル側ではなく、piにローカルの画像ファイルを画像ブロックとしてモデルへ送る仕組みが無かったのだと思う。
キャプチャはブラウザ系のツールが画像を返すので見えていて、ファイルを指した読み込みには同じ仕組みが無かった。
core の read_image はそれを足すもので、手順の上ではpiにも付けられる。
テキスト専用モデルに繋ぐ場合
core が返すのは画像ブロックなので、呼ぶ側が画像を受け取れない場合は QWEN_MM_NATIVE_MODE=1 のままでは使えない。
そのために QWEN_MM_NATIVE_MODE=0 があって、こっちにすると画像の代わりに、DashScopeのVLモデルが付けた説明文(キャプション)が同じ場所に返ってくる。
設定のドキュメントには、このモードだとツールの結果画像(ローカルファイルや画面のスクリーンショットも含む)が設定したVLエンドポイントへ送られるので、共有してよいデータにだけ使え、と明記してある。
キャプションに失敗したときは Visual content unavailable のテキストが入って、base64を漏らしたり黙って画像を落としたりはしない。
ローカルのQwen3.8-27Bみたいに画像入力を持つモデルをOpenAI互換で立てている場合は、呼び出し側のハーネスがMCPの画像ブロックをモデルAPIの画像入力へ転送できて、エンドポイントもその形式を受け付けるなら、NATIVE_MODE=1 のまま動く。
手動設定の案内には、DeepSeek Harnessは画像ブロックを捨てる、Hermes Agentは画像のピクセルをプロバイダへ送らない、という実例が書いてあって、モデルが画像入力を持っていてもハーネス側で画像が届かない場合がある。
それができないなら、DASHSCOPE_BASE_URL と QWEN_MM_API_VL_MODEL を自前のエンドポイントとモデルに向けて、認証がいらなくても DASHSCOPE_API_KEY に空でないプレースホルダーを入れて NATIVE_MODE=0 にする。
ちなみに pyproject.toml では、core の依存パッケージは mcp・anyio・pillow・openai の4つに可視化用の extras(pypdfium2、resvg-py、pandas、trimesh、pyrender、playwright など)を足したもので、QwenやDashScopeの名前が付いたパッケージは1つも入っていない。openaiはテキスト専用モードのキャプション用。