0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Model Context Protocol完全解説 30日間シリーズ - Day 19【MCP応用 #19】画像・メディアファイル対応:バイナリデータのMCP経由アクセス

Last updated at Posted at 2025-09-15

はじめに

この記事は、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;
};

🎯 まとめ:バイナリデータ対応のベストプラクティス

技術的なポイント:

  1. Base64エンコーディング: バイナリデータは必ずBase64エンコードして文字列として扱う
  2. メタデータの充実: ファイル形式、サイズ、解像度などの情報を豊富に提供
  3. 効率的なデータ転送: 大きなファイルの場合は、必要な部分のみを送信する仕組みを検討
  4. 適切なMIMEタイプ: ファイル形式に対応した正確なMIMEタイプを指定

UX設計のポイント:

  1. 段階的な情報開示: まずメタデータを提供し、詳細が必要な場合のみ実データを送信
  2. 専用ツールの提供: ファイル形式に応じた専門的な解析機能を提供
  3. エラーハンドリング: ファイルの存在確認や形式検証を適切に実装
  4. パフォーマンス配慮: 大容量ファイルの場合はストリーミングや部分読み込みを検討

セキュリティの考慮:

  1. ファイルアクセス制限: 許可されたディレクトリ内のファイルのみアクセス可能に
  2. ファイル形式検証: 拡張子だけでなく、実際のファイルヘッダーも確認
  3. サイズ制限: 過度に大きなファイルのアップロードを制限

このテクニックにより、MCPがテキスト以外のデータも効率的に扱えることが確認できました。この機能は、マルチモーダルAIとの連携を可能にし、よりリッチなアプリケーションを構築するための重要な基盤となります。

次回は、MCP通信の可視化と問題発見手法として、ログ・監視機能の追加について解説します。お楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?