MCP(Model Context Protocol)を本番に投入し始めて気づくのが、ファイルアップロードの仕様の薄さです。仕様レベルではバイナリ転送の標準モードがまだ提案段階で、SEP-1306(Binary Mode Elicitation for File Uploads)も2026年5月時点で「draft」のままです。
「採用待ち」とは言え、業務はそれを待ってくれません。確定申告の領収書、Slackのスクショ、Notionの添付。MCP経由で扱いたいファイルは日々増えています。
私は3月にQiitaで「MCPファイルアップロードを7サービスで検証してみた」を書きました。今回はその続編として、今すぐ本番で使える4つの回避策を整理します。
なぜMCPでファイルアップロードが面倒なのか
最初に問題を共通理解にしておきます。MCPはJSON-RPCベースのプロトコルで、ツール呼び出しの引数も基本は文字列・数値・配列です。バイナリの直接転送は想定外です。
elicitation(追加情報の問い合わせ)は2025年のアップデートで追加されましたが、サポートされているのは「form」と「URL」モードだけ。SEP-1306が提案する「binary」モードはまだ採用されていません。
つまりバイナリは「プロトコルの外」で運ぶしかありません。これが回避策が必要な根本理由です。
SEP-1306の最新状態はGitHub Issue #1306で確認できます。2026年5月時点ではdraftのまま、accepted入りしていません。
回避策1 署名付きURL方式(推奨)
最もクリーンで本番運用に適したパターンです。MCPは「指示」だけを運び、バイナリ本体はクライアントが直接ストレージに送ります。
Step 1: MCPで署名付きアップロードURLを発行
Step 2: クライアントがそのURLに直接ファイルをPUT
Step 3: MCPで完了通知
実装コードはこんな形になります。
# Step 1: MCP側で署名付きURLを取得
upload_info = mcp_tool.call("get_upload_url", {
"filename": "receipt.jpg",
"content_type": "image/jpeg"
})
# → {"upload_url": "https://api.example.com/upload/abc?token=xyz",
# "expires_in": 300}
# Step 2: 直接アップロード(MCPの外側)
import requests
with open("receipt.jpg", "rb") as f:
requests.put(
upload_info["upload_url"],
data=f,
headers={"Content-Type": "image/jpeg"},
)
# Step 3: MCPで完了を通知
mcp_tool.call("confirm_upload", {"upload_id": "abc"})
メリットは大きく3つです。
- セキュア: 署名付きで期限が切れる、漏洩リスクが低い
- 大容量OK: ストレージへの直送なのでサイズ制限がほぼない
- MCPの設計思想を壊さない: バイナリを文字列化しない
デメリットは1つだけ。サービス側APIが署名付きURLに対応している必要がある点です。AWS S3、Cloudflare R2、Google Cloud Storage、Supabase Storageなど、主要ストレージはほぼ対応しています。
実装例 AWS S3の場合
import boto3
def get_presigned_url(filename: str, content_type: str) -> dict:
s3 = boto3.client("s3")
url = s3.generate_presigned_url(
ClientMethod="put_object",
Params={
"Bucket": "my-bucket",
"Key": f"uploads/{filename}",
"ContentType": content_type,
},
ExpiresIn=300,
)
return {"upload_url": url, "expires_in": 300}
このコードをMCPツールとしてラップするだけで、署名付きURL方式は完成します。
回避策2 CLI外部スクリプト併用
MCPで「対話的にやりたい」けど「ファイルだけはMCPで運べない」、という現実的なケースに効くハイブリッドです。私が確定申告で実際に使った構成です。
#!/bin/bash
# MCP + CLI ハイブリッド経費処理スクリプト
# Step 1: MCPで取引を一括作成(Claude Desktop経由)
echo "MCPで取引を作成してください"
echo "完了したら取引IDをtransaction_ids.txtに保存"
read -p "準備できたらEnter: "
# Step 2: 領収書を一括アップロード
while IFS=, read -r tx_id receipt_file; do
python3 upload_receipt.py \
--transaction-id "$tx_id" \
--file "receipts/$receipt_file"
echo "OK: $receipt_file -> TX:$tx_id"
sleep 1
done < transaction_ids.txt
メリットは「確実に動く」の一点に尽きます。既存のAPIクライアントをそのまま使えるので、検証時間がほぼゼロです。
デメリットは2つ。
- MCPの「対話的な操作」体験が半減する
- スクリプトの保守が必要、Claude任せにできない
それでも、本番で確実にデータを動かしたい場面では、私はこれを選びます。MCPは「人間が判断する部分」、CLIは「機械的に動かす部分」と役割分担すれば、それぞれの強みが活きます。
回避策3 base64エンコード
小さいファイル(数百KB以下)に限定して使えるパターンです。
import base64
with open("receipt.jpg", "rb") as f:
encoded = base64.b64encode(f.read()).decode()
result = mcp_tool.call("upload_receipt", {
"data": encoded,
"filename": "receipt.jpg",
"content_type": "image/jpeg",
})
実装は最小限で済みます。しかしトークン消費が爆発します。
| 元ファイル | base64後 | LLMトークン換算 |
|---|---|---|
| 100KB画像 | 約133KB | 約3万トークン |
| 1MB画像 | 約1.33MB | 約30万トークン |
| 5MBドキュメント | 約6.7MB | 約150万トークン |
Claude 3.5 Sonnetの1セッション上限は200Kトークンです。1MBの画像を1枚送るだけで、コンテキストの大半を食い潰します。
base64方式はサンプル動作確認に使うのは便利ですが、本番投入は推奨しません。トークン課金が想定の10倍に膨らむ事故が起きやすいです。
回避策4 共有ストレージ経由
MCPサーバーとクライアントが同じファイルシステムを参照できる場合のみ使えるパターンです。Claude Desktopとローカルのfreee-mcpのような構成です。
# クライアント側
shutil.copy("receipt.jpg", "/shared/receipts/receipt.jpg")
# MCP経由でパスだけを渡す
mcp_tool.call("attach_file", {
"path": "/shared/receipts/receipt.jpg",
})
メリットは「実装ゼロ」。ただし制約も明確です。
- Docker構成では使えない(ボリュームマウント必須)
- リモートMCPサーバーでは使えない
- ファイルシステムのアクセス権設計が複雑になる
開発環境でPoCを動かす時には便利です。本番への持ち込みは要注意です。
4つの回避策の使い分け
ここまで紹介した4つの選択基準を、私の視点でまとめます。
| 回避策 | 推奨度 | 適用シーン |
|---|---|---|
| 署名付きURL | ◎ | 本番運用、容量大、複数ユーザー |
| CLI併用 | ◯ | バッチ処理、確実性重視 |
| base64 | △ | 数百KB以下、開発検証のみ |
| 共有ストレージ | △ | ローカルPoC、単一ユーザー |
「とりあえずbase64」を選びがちですが、それが事故の元です。私は新規プロジェクトでは原則として署名付きURL方式から検討を始めます。本当にbase64しか手段がない場合は、サイズ上限(例 50KB)を厳格に設けます。
セキュリティ設計の3つの基本
回避策を選ぶときに、同時にセキュリティ設計も決めるべきです。私が必ず入れている3点を共有します。
1 トークン管理は環境変数のみ
# 設定ファイルへのハードコード厳禁
{
"env": {
"API_KEY": "sk-xxxxxxxxxxxxx"
}
}
# 良い例: 環境変数を参照
export FREEE_CLIENT_ID="your_id"
export FREEE_CLIENT_SECRET="your_secret"
MCP設定ファイルがGit管理に紛れ込む事故を、私は2回ほど目撃しました。うち1回は自分のリポジトリで冷や汗をかいた話なので、人のことは言えません。.envを分離し、.gitignoreで必ず除外します。
2 権限の最小化
サービスが提供するMCPツール全部を有効化していませんか。freeeなら全270ツールのうち、確定申告に必要なのは10ツール程度です。
{
"mcpServers": {
"freee": {
"allowedTools": [
"create_deal",
"list_deals",
"get_deal",
"list_account_items",
"list_partners",
"list_taxes"
]
}
}
}
これだけで権限を96%削減できます。攻撃面積(attack surface)を最小化する基本です。
3 人間の承認ゲート
機密操作には必ず人間の承認を挟みます。
| 操作 | 自動実行 | 人間承認 |
|---|---|---|
| 取引参照 | ✅ | |
| 取引作成 | ✅ | |
| 取引削除 | ✅ 必須 | |
| バッチ削除 | 🚫 禁止 | |
| APIキー変更 | ✅ 必須 |
「速度」と「安全」は両立できます。重要な分岐だけに承認ゲートを置けば、生産性は維持しつつ事故は防げます。
トークンコスト最適化
回避策と並んで、地味だが効くのがトークンコストの最適化です。MCPツールの説明文をダイエットするだけで、月のAPI課金が大きく変わります。
// 冗長(約80トークン)
{
"description": "freeeの会計APIを使用して指定された会社IDに対して新しい取引(仕訳)を作成します。金額、日付、勘定科目、取引先名、メモなどを指定できます"
}
// 簡潔(約20トークン)
{
"description": "取引を作成。引数: amount, date, account_item, partner"
}
この差は1ツールあたり60トークン。10ツールあれば600トークン、毎リクエスト乗ります。月1,000リクエストなら月60万トークンの差です。
SEP-1306が来るまでの戦略
MCP仕様の正式なバイナリサポートを待つのは、私は半年から1年と見積もっています。SEPがdraftからacceptedに上がり、各クライアントが対応するまでのリードタイムを考えると、2026年中に「公式仕様で安定」とはいかなそうです。
それまでは本記事の4つの回避策で前進するのが現実解です。優先順位は次の通りです。
## 私の推奨ロードマップ
1. すべての新規MCPプロジェクトは「署名付きURL方式」から設計
2. 既存のbase64実装はサイズ上限を50KBに即引下げ
3. 共有ストレージ実装はDocker化と同時に廃止
4. CLIハイブリッドは現実解として温存、棄てない
5. SEP-1306がacceptedになったら段階移行を開始
完璧なプロトコルを待つな、今あるもので安全に動かせ。これが私のMCP本番運用のスタンスです。…とは言いつつ、SEP-1306がacceptedになった日には誰よりも早く乗り換える気でいます。
まとめ
MCPのファイルアップロードは仕様未成熟ですが、4つの回避策で本番運用は可能です。
- 署名付きURL方式: 本番の第一選択
- CLI併用: 確実に動く現実解
- base64: 開発検証のみ、本番は避ける
- 共有ストレージ: ローカルPoC専用
セキュリティは「環境変数管理」「権限最小化」「人間承認ゲート」の3点を守れば、最低限の安全性は確保できます。トークンコストはツール説明文のダイエットで大幅に下げられます。
あなたの現場では、どの回避策を採用していますか。私は基本「署名付きURL方式」一択で設計し始めるようになりました。読者のMCP本番投入に、本記事が一助になれば嬉しいです。
MCPセキュリティの体系的な理解には、Zenn Book「MCPセキュリティ実践」をどうぞ。本記事の続編として、SEP-1306やFileContent構想の展望を9章で扱っています。
https://zenn.dev/kenimo49/books/mcp-security-practice
参考
- SEP-1306: Binary Mode Elicitation for File Uploads(GitHub Issue, draft)
- The 2026 MCP Roadmap(Model Context Protocol Blog)
- MCP Specification 2025-11-25(公式仕様書)
- 前作: 「MCPファイルアップロードを7サービスで検証してみた」(Qiita, 2026-03)
