はじめに
この記事は、QiitaのModel Context Protocol(以下、MCP)解説シリーズの第19回です。
今回は、MCPを応用して、画像や音声などのバイナリデータを扱う方法を解説します。これにより、LLMはテキストだけでなく、メディアファイルも活用できるようになり、アプリケーションの幅が大きく広がります。
🖼️ なぜバイナリデータの扱いが重要なのか?
MCPのResources機能は、テキストファイルだけでなく、あらゆる種類のデータを公開できます。バイナリデータ(非テキストデータ)をMCP経由で扱うことは、以下のようなユースケースで非常に重要です。
- 画像認識: LLMに画像を渡し、「この画像に写っているものを説明して」といったタスクを実行させる
- ドキュメント処理: PDFファイルを渡し、その内容を解析・要約させる
- メディア管理: 画像のメタデータ抽出や、ファイル形式の確認を行う
- マルチモーダルな応答: LLMが画像の内容を理解し、適切な回答を生成する
MCPでは、バイナリデータをBase64エンコードして扱うのが標準的なアプローチです。
📝 実装:画像とPDFファイルを公開し、LLMに解析させる
今回は、ローカルの画像ファイルとPDFファイルをMCPのResourcesとして公開し、LLMにその内容を説明させるシナリオを実装します。
ステップ1:プロジェクトのセットアップ
TypeScriptと、画像解析用のライブラリを使用します。
mkdir mcp-binary-data
cd mcp-binary-data
npm init -y
npm install typescript ts-node @modelcontextprotocol/sdk zod sharp pdf-parse
npx tsc --init
次に、公開したいファイルをプロジェクトフォルダに用意します。
-
sample-image.jpg- テスト用の画像 -
sample-document.pdf- テスト用のPDF
ステップ2:MCPサーバーのコード
server.tsファイルを作成し、以下のコードを貼り付けてください。
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
Tool,
Resource,
} from '@modelcontextprotocol/sdk/types.js';
import { readFileSync, existsSync, statSync } from 'fs';
import { resolve, extname } from 'path';
import { z } from 'zod';
import sharp from 'sharp';
import pdf from 'pdf-parse';
const server = new Server(
{
name: 'binary-data-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// サポートするファイル形式とMIMEタイプのマッピング
const mimeTypes: { [key: string]: string } = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.json': 'application/json',
};
// ファイルのメタデータを取得する関数
async function getFileMetadata(filePath: string) {
const stats = statSync(filePath);
const ext = extname(filePath).toLowerCase();
const mimeType = mimeTypes[ext] || 'application/octet-stream';
let metadata: any = {
size: stats.size,
lastModified: stats.mtime.toISOString(),
mimeType,
};
// 画像の場合、詳細情報を取得
if (mimeType.startsWith('image/')) {
try {
const imageMetadata = await sharp(filePath).metadata();
metadata = {
...metadata,
width: imageMetadata.width,
height: imageMetadata.height,
format: imageMetadata.format,
channels: imageMetadata.channels,
};
} catch (error) {
console.error('画像メタデータの取得に失敗:', error);
}
}
return metadata;
}
// Base64エンコードを行う関数
function encodeToBase64(filePath: string): string {
const fileBuffer = readFileSync(filePath);
return fileBuffer.toString('base64');
}
// ファイル解析用のツールスキーマ
const analyzeFileSchema = z.object({
resource_uri: z.string().describe('解析するリソースのURI'),
});
const extractTextSchema = z.object({
resource_uri: z.string().describe('テキスト抽出するリソースのURI'),
});
// 利用可能なファイルのリスト(実際のプロジェクトではデータベースやファイルシステムから動的に取得)
const availableFiles = [
{ name: 'sample-image.jpg', description: 'サンプル画像ファイル' },
{ name: 'sample-document.pdf', description: 'サンプルPDFドキュメント' },
];
// リソースのリスト
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources: Resource[] = [];
for (const file of availableFiles) {
const filePath = resolve(file.name);
if (existsSync(filePath)) {
const metadata = await getFileMetadata(filePath);
resources.push({
uri: `binary://${file.name}`,
name: file.name,
description: `${file.description} (${Math.round(metadata.size / 1024)}KB)`,
mimeType: metadata.mimeType,
});
}
}
return { resources };
});
// リソースの読み取り
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
// URIからファイル名を抽出
const fileName = uri.replace('binary://', '');
const filePath = resolve(fileName);
if (!existsSync(filePath)) {
throw new Error(`ファイルが見つかりません: ${fileName}`);
}
const metadata = await getFileMetadata(filePath);
// バイナリデータをBase64エンコード
const base64Data = encodeToBase64(filePath);
// メタデータ情報をテキストとして提供
const metadataText = `ファイル情報:
- ファイル名: ${fileName}
- サイズ: ${Math.round(metadata.size / 1024)}KB
- MIMEタイプ: ${metadata.mimeType}
- 最終更新: ${metadata.lastModified}
${metadata.width ? `- 画像サイズ: ${metadata.width}x${metadata.height}px` : ''}
${metadata.format ? `- 画像フォーマット: ${metadata.format}` : ''}
Base64データ: ${base64Data.substring(0, 100)}...(省略)`;
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: metadataText,
},
],
};
} catch (error) {
throw new Error(`リソース読み取りエラー: ${error}`);
}
});
// ツールのリスト
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'analyze_file',
description: 'ファイルの詳細な解析を行います(画像の場合は内容分析、PDFの場合はテキスト抽出)',
inputSchema: analyzeFileSchema,
},
{
name: 'extract_text',
description: 'PDFファイルからテキストを抽出します',
inputSchema: extractTextSchema,
},
] satisfies Tool[],
};
});
// ツール実行のハンドラー
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'analyze_file': {
const parsed = analyzeFileSchema.parse(args);
const fileName = parsed.resource_uri.replace('binary://', '');
const filePath = resolve(fileName);
if (!existsSync(filePath)) {
throw new Error(`ファイルが見つかりません: ${fileName}`);
}
const metadata = await getFileMetadata(filePath);
const ext = extname(fileName).toLowerCase();
let analysisResult = `ファイル解析結果:\n\n`;
analysisResult += `基本情報:\n`;
analysisResult += `- ファイル名: ${fileName}\n`;
analysisResult += `- ファイルサイズ: ${Math.round(metadata.size / 1024)}KB\n`;
analysisResult += `- MIMEタイプ: ${metadata.mimeType}\n`;
if (metadata.mimeType.startsWith('image/')) {
analysisResult += `\n画像情報:\n`;
analysisResult += `- 解像度: ${metadata.width}x${metadata.height}px\n`;
analysisResult += `- フォーマット: ${metadata.format}\n`;
analysisResult += `- チャンネル数: ${metadata.channels}\n`;
// 簡単な画像分析(実際のプロジェクトではAI画像認識APIを使用)
const aspectRatio = metadata.width / metadata.height;
if (aspectRatio > 1.5) {
analysisResult += `- 特徴: 横長の画像(風景写真の可能性が高い)\n`;
} else if (aspectRatio < 0.7) {
analysisResult += `- 特徴: 縦長の画像(ポートレートの可能性が高い)\n`;
} else {
analysisResult += `- 特徴: ほぼ正方形の画像\n`;
}
}
return {
content: [
{
type: 'text',
text: analysisResult,
},
],
};
}
case 'extract_text': {
const parsed = extractTextSchema.parse(args);
const fileName = parsed.resource_uri.replace('binary://', '');
const filePath = resolve(fileName);
if (!existsSync(filePath)) {
throw new Error(`ファイルが見つかりません: ${fileName}`);
}
const ext = extname(fileName).toLowerCase();
if (ext !== '.pdf') {
throw new Error('このツールはPDFファイルのみをサポートしています');
}
const fileBuffer = readFileSync(filePath);
const pdfData = await pdf(fileBuffer);
return {
content: [
{
type: 'text',
text: `PDFテキスト抽出結果:\n\nページ数: ${pdfData.numpages}\n\n抽出テキスト:\n${pdfData.text}`,
},
],
};
}
default:
throw new Error(`未知のツール: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `エラー: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// サーバーの開始
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Binary data MCP server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
👆 ポイント解説
1. Base64エンコーディングによるバイナリデータ処理
- MCPではバイナリデータを直接送信するのではなく、Base64エンコードして文字列として扱うのが推奨されています
- メタデータと実際のデータを分離して提供することで、効率的な処理が可能
2. メタデータの充実
- ファイルサイズ、更新日時、MIMEタイプなどの基本情報を提供
- 画像の場合は解像度やフォーマット情報も追加
- LLMが適切な処理を判断するための情報を豊富に提供
3. 専用ツールによる高度な解析
-
analyze_file: ファイルの詳細解析(メタデータ + 簡単な内容分析) -
extract_text: PDFからのテキスト抽出 - 実際のプロジェクトではAI画像認識APIなどと連携可能
4. エラーハンドリングとファイル検証
- ファイルの存在確認
- サポートされているファイル形式の検証
- 適切なエラーメッセージの提供
🚀 動作の確認
1.サンプルファイルの準備
# テスト用の画像ファイル(適当な画像をsample-image.jpgとして配置)
# テスト用のPDFファイル(適当なPDFをsample-document.pdfとして配置)
2.サーバーを起動する
npx ts-node server.ts
3.Claude Desktopでテストする
- 例1: 「利用可能なリソースを教えて」→ ファイルリストが表示される
- 例2: 「sample-image.jpgの情報を教えて」→ 画像のメタデータが表示される
- 例3: 「analyze_fileツールでsample-image.jpgを解析して」→ 詳細な画像分析結果
- 例4: 「extract_textツールでsample-document.pdfからテキストを抽出して」→ PDFの内容が表示される
🎯 実用的な使用例
画像ライブラリ管理
// 画像フォルダをスキャンして自動的にリソースとして公開
const imageFolder = './images';
const imageFiles = fs.readdirSync(imageFolder)
.filter(file => /\.(jpg|jpeg|png|gif)$/i.test(file));
ドキュメント処理システム
// PDFドキュメントの自動分類と要約
const documentAnalyzer = {
async analyzeDocument(filePath: string) {
const pdfData = await pdf(readFileSync(filePath));
// AI APIを使用してドキュメントを分析・分類
return {
category: 'business_report',
summary: '...',
keyPoints: ['...', '...']
};
}
};
マルチモーダルAIとの連携
// 画像認識APIとの連携例
const analyzeImageWithAI = async (imageBase64: string) => {
// OpenAI Vision API や Google Cloud Vision API と連携
const response = await visionAPI.analyze({
image: imageBase64,
features: ['LABEL_DETECTION', 'TEXT_DETECTION']
});
return response;
};
🎯 まとめ:バイナリデータ対応のベストプラクティス
技術的なポイント:
- Base64エンコーディング: バイナリデータは必ずBase64エンコードして文字列として扱う
- メタデータの充実: ファイル形式、サイズ、解像度などの情報を豊富に提供
- 効率的なデータ転送: 大きなファイルの場合は、必要な部分のみを送信する仕組みを検討
- 適切なMIMEタイプ: ファイル形式に対応した正確なMIMEタイプを指定
UX設計のポイント:
- 段階的な情報開示: まずメタデータを提供し、詳細が必要な場合のみ実データを送信
- 専用ツールの提供: ファイル形式に応じた専門的な解析機能を提供
- エラーハンドリング: ファイルの存在確認や形式検証を適切に実装
- パフォーマンス配慮: 大容量ファイルの場合はストリーミングや部分読み込みを検討
セキュリティの考慮:
- ファイルアクセス制限: 許可されたディレクトリ内のファイルのみアクセス可能に
- ファイル形式検証: 拡張子だけでなく、実際のファイルヘッダーも確認
- サイズ制限: 過度に大きなファイルのアップロードを制限
このテクニックにより、MCPがテキスト以外のデータも効率的に扱えることが確認できました。この機能は、マルチモーダルAIとの連携を可能にし、よりリッチなアプリケーションを構築するための重要な基盤となります。
次回は、MCP通信の可視化と問題発見手法として、ログ・監視機能の追加について解説します。お楽しみに!