メインコンテンツへスキップ
見出し画像

PDFが読めないClaudeに「目」を与えた!自作MCPで100ページ瞬間解析を実現した話

    こんにちは!YaroTechです。

    「このPDFファイルの内容を要約してください」
    「申し訳ございませんが、PDFファイルを直接読み込むことはできません」

    この会話、Claude Desktopユーザーなら誰もが経験したことがあるはずです。

    でも、もうその苦労は終わりです。PDF読み込み専用のMCPを自作したら、100ページのPDFがたった5秒で読み込めるようになりました!今日はその実装方法と、実際に遭遇したトラブルも含めて全公開します。

    ※コメントで「公開希望!」があったら、自作MCPのコードを公開します!


    🖥️ 実行環境

    • PC: Microsoft Surface Laptop 7th(Snapdragon X Elite / ARM64)

    • OS: Windows 11 Pro 24H2(ARM64版)

    • Claude Desktop: 最新版(Ver. 0.12.55 (d55c63))

    • Node.js: v20.11.0以降

    • TypeScript: 5.7.2

    • 必要な空き容量: 約100MB


    🎯 この記事で得られること

    • PDFを直接Claudeに読み込ませる方法

    • TypeScriptでMCPを自作する完全手順

    • pdf2jsonを使った実装テクニック

    • ファイルパスの落とし穴と解決策

    • 実務で使える5つの活用パターン


    📊 実際の成果

    Before: PDF100ページをコピペ → 100分(1ページ1分×100)
    After: MCP経由で一括読み込み → 5分5秒(読み込み5秒+分析5分)
    削減時間: 94.9%削減!(ほぼ20倍速)

    しかも、書式も表も崩れることなく、完璧に内容を理解してくれます。


    🚀 実践内容

    アプローチ1: PDF読み込みMCPの基本実装

    まず、MCPの基本構造を作ります。今回は5つの主要機能を実装しました:

    1. pdf_read_text - PDFからテキスト抽出

    2. pdf_get_metadata - メタデータ取得(作成者、日時等)

    3. pdf_extract_pages - 特定ページ範囲の抽出

    4. pdf_search_text - PDF内テキスト検索

    5. pdf_list_files - ディレクトリ内PDF一覧

    パッケージ構成(package.json)

    {
      "name": "pdf-reader-mcp",
      "version": "0.1.0",
      "description": "PDF読み取りMCPサーバー",
      "main": "dist/index.js",
      "type": "module",
      "dependencies": {
        "@modelcontextprotocol/sdk": "^1.0.4",
        "pdf2json": "^3.1.4",
        "glob": "^11.0.0"
      },
      "devDependencies": {
        "@types/node": "^22.10.2",
        "typescript": "^5.7.2"
      }
    }

    基本構造の実装コード(抜粋)

    // src/index.ts
    import { Server } from '@modelcontextprotocol/sdk/server/index.js';
    import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
    import PDFParser from 'pdf2json';
    import { glob } from 'glob';
    
    // PDFキャッシュ(メモリ効率化)
    interface PDFCache {
      text: string;
      pageCount: number;
      metadata: any;
    }
    const pdfCache = new Map<string, PDFCache>();
    
    // MCPサーバーの作成
    const server = new Server(
      {
        name: 'pdf-reader-mcp',
        version: '0.1.0',
      },
      {
        capabilities: {
          tools: {}
        }
      }
    );
    
    // PDFファイルを読み込む関数
    async function readPDF(filePath: string): Promise<PDFCache> {
      // キャッシュチェック
      if (pdfCache.has(filePath)) {
        return pdfCache.get(filePath)!;
      }
    
      return new Promise((resolve, reject) => {
        const pdfParser = new PDFParser();
        
        pdfParser.on('pdfParser_dataError', (errData: any) => {
          reject(new Error(`PDF解析エラー: ${errData.parserError}`));
        });
        
        pdfParser.on('pdfParser_dataReady', (pdfData: any) => {
          try {
            let fullText = '';
            const pageTexts: string[] = [];
            
            if (pdfData.Pages) {
              pdfData.Pages.forEach((page: any) => {
                let pageText = '';
                if (page.Texts) {
                  page.Texts.forEach((text: any) => {
                    if (text.R) {
                      text.R.forEach((r: any) => {
                        if (r.T) {
                          // URLデコードしてテキストを取得
                          const decodedText = decodeURIComponent(r.T);
                          pageText += decodedText + ' ';
                        }
                      });
                    }
                  });
                }
                pageTexts.push(pageText.trim());
                fullText += pageText + '\n\n';
              });
            }
            
            const result: PDFCache = {
              text: fullText.trim(),
              pageCount: pageTexts.length,
              metadata: pdfData.Meta || {}
            };
            
            // キャッシュに保存(最大10ファイル)
            if (pdfCache.size >= 10) {
              const firstKey = pdfCache.keys().next().value;
              if (firstKey) pdfCache.delete(firstKey);
            }
            pdfCache.set(filePath, result);
            
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
        
        pdfParser.loadPDF(filePath);
      });
    }

    効果: 基本的なPDF読み込みが可能に!キャッシュで2回目以降は瞬速!


    アプローチ2: 実用的な検索機能の実装

    単に読み込むだけでなく、実務で使える検索機能を追加しました。

    PDF内テキスト検索の実装

    server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      
      if (name === 'pdf_search_text') {
        const { file_path, search_text, case_sensitive = false } = args;
        
        try {
          const pdfData = await readPDF(file_path);
          const searchPattern = case_sensitive 
            ? search_text 
            : search_text.toLowerCase();
          
          const lines = pdfData.text.split('\n');
          const results: Array<{line: number, text: string}> = [];
          
          lines.forEach((line: string, index: number) => {
            const lineToSearch = case_sensitive 
              ? line 
              : line.toLowerCase();
            
            if (lineToSearch.includes(searchPattern)) {
              // 前後の文脈も含めて返す
              const context = lines.slice(
                Math.max(0, index - 1),
                Math.min(lines.length, index + 2)
              ).join('\n');
              
              results.push({
                line: index + 1,
                text: context
              });
            }
          });
          
          return {
            content: [{
              type: "text",
              text: results.length > 0 
                ? `検索結果: ${results.length}件\n\n` + 
                  results.map(r => `【${r.line}行目】\n${r.text}`).join('\n\n---\n\n')
                : `「${search_text}」は見つかりませんでした。`
            }]
          };
        } catch (error: any) {
          return {
            content: [{
              type: "text",
              text: `エラー: ${error.message}`
            }]
          };
        }
      }
    });

    複数PDFの一覧表示

    if (name === 'pdf_list_files') {
      const { directory, recursive = true } = args;
      
      try {
        const pattern = recursive 
          ? path.join(directory, '**', '*.pdf')
          : path.join(directory, '*.pdf');
        
        const files = await glob(pattern, {
          windowsPathsNoEscape: true
        });
        
        const fileInfos = await Promise.all(
          files.map(async (file) => {
            const stats = await fs.stat(file);
            return {
              path: file,
              size: `${(stats.size / 1024 / 1024).toFixed(2)} MB`,
              modified: stats.mtime.toISOString()
            };
          })
        );
        
        return {
          content: [{
            type: "text",
            text: `PDFファイル一覧(${fileInfos.length}件):\n\n` +
                  fileInfos.map(f => 
                    `📄 ${path.basename(f.path)}\n` +
                    `   パス: ${f.path}\n` +
                    `   サイズ: ${f.size}\n` +
                    `   更新日: ${f.modified}`
                  ).join('\n\n')
          }]
        };
      } catch (error: any) {
        return {
          content: [{
            type: "text",
            text: `エラー: ${error.message}`
          }]
        };
      }
    }

    効果: 契約書の特定条項を秒速で発見!複数PDFの管理も楽々!


    アプローチ3: Claude Desktopへの統合設定

    作ったMCPをClaude Desktopで使えるように設定します。

    設定ファイル(claude_desktop_config.json)

    {
      "mcpServers": {
        "pdf-reader": {
          "command": "node",
          "args": ["C:\\Users\\YourName\\mcp-projects\\pdf-reader-mcp\\dist\\index.js"]
        }
      }
    }

    設定ファイルの場所:

    • Windows: `%APPDATA%\Claude\claude_desktop_config.json`

    注意: パスは必ずバックスラッシュを2つ(\)にする!


    ⚠️ つまずきポイントと解決策

    1. 改行文字(\n)がファイルパスに入る問題 😱

    問題: Claudeがファイルパスを渡すときに、なぜか改行文字が入ることがある

    例: "C:\Users\YourName\Documents\n\sample.pdf"
        → \n が改行として解釈されてファイルが見つからない!

    解決策: パスの正規化処理を追加

    // ファイルパスの正規化
    function normalizePath(filePath: string): string {
      // 改行文字を除去
      let normalized = filePath.replace(/\n/g, '');
      
      // バックスラッシュの正規化
      normalized = normalized.replace(/\\/g, '/');
      
      // 余分な空白を除去
      normalized = normalized.trim();
      
      return normalized;
    }
    
    // 使用時
    const cleanPath = normalizePath(args.file_path);
    const pdfData = await readPDF(cleanPath);

    2. 日本語がファイルパスに含まれるとエラー 🇯🇵

    問題: 「C:\Users\YourName\ドキュメント\議事録.pdf」のような日本語パスでサーバーエラー

    解決策: UTF-8エンコーディングとWindows対応

    // globオプションでWindows対応
    const files = await glob(pattern, {
      windowsPathsNoEscape: true,  // Windowsパス対応
      absolute: true                // 絶対パスで取得
    });
    
    // pdf2jsonは内部でUTF-8処理されるが、
    // ファイルシステムアクセス時は注意
    import { fileURLToPath } from 'url';
    
    // 日本語パスを含むファイルの存在確認
    try {
      await fs.access(filePath);
    } catch {
      throw new Error(`ファイルが見つかりません: ${filePath}`);
    }

    3. pdf-parseからpdf2jsonへの移行

    問題: 最初はpdf-parseを使おうとしたが、TypeScriptの型定義エラーが解決できず

    error TS7016: Could not find a declaration file for module 'pdf-parse'

    解決策: pdf2jsonに変更して解決!

    npm uninstall pdf-parse
    npm install pdf2json

    pdf2jsonの方が:

    • TypeScript対応が良好

    • 日本語処理も安定

    • メモリ効率も良い


    💡 実務での活用例

    1. 契約書レビューの自動化

    実際の使用例:
    「C:\Users\YourName\Documents\contracts フォルダ内の全契約書から
     解約条項に関する部分を抽出してください」
    
    結果:
    10件の契約書から該当箇所を5分で抽出完了!

    2. 技術仕様書の要約

    実際の使用例:
    「この200ページの仕様書から、APIエンドポイント一覧を作成してください」
    
    結果:
    45個のエンドポイントを自動で表形式に整理!

    3. 議事録の統合

    実際の使用例:
    「今月の議事録PDFをすべて読み込んで、決定事項だけをまとめてください」
    
    結果:
    15回分の会議から重要決定事項23個を抽出!

    🔗 関連記事

    今回の内容に関連する過去記事もぜひご覧ください:


    📝 まとめ

    PDF読み込みMCPを自作することで、Claudeの可能性が大きく広がりました。

    実現できたこと

    • ✅ 100ページのPDFを5秒で読み込み

    • ✅ pdf2jsonによる安定した処理

    • ✅ ファイルパスの落とし穴を克服

    • ✅ 複数PDF横断検索

    • ✅ キャッシュによる高速化

    まだ残る課題

    • ⚠️ スキャンPDF(画像PDF)のOCR未対応

    • ⚠️ パスワード付きPDFは読めない

    • ⚠️ 日本語パスは要注意

    次のステップ

    1. OCR機能の追加(tesseract.js検討中)

    2. PDFの結合・分割機能

    3. クラウドストレージ連携

    もしこのMCPに興味がある方がいらっしゃったら、GitHubで公開することも検討します!コメントで教えてください。


    🚀 次回予告

    明日はついにDay30!30日間の集大成として、「30日間毎日投稿して分かった、続けるコツと得られたもの」をお届けします。

    挫折しそうになった瞬間、モチベーション維持の秘訣、そして予想外の収穫まで、すべて正直にお話しします!


    #YaroTech #ClaudeDesktop #MCP #PDF #TypeScript #pdf2json #業務効率化 #自動化


    📌 この記事が参考になったら、ぜひ「スキ」をお願いします!

    GitHubでの公開を希望される方は、コメントで「公開希望!」と教えてください。コメント確認したら、すぐに公開準備を始めます!

    「改行文字問題」や「日本語パス問題」で困っている方がいたら、コメントで詳しい解決策をお伝えします!

     
     
    生成AIの感動を共有したいYaroTechです✨ 常識より実験。正解より行動。 生成AI時代を、先に試す傾奇者。 📝 470日間継続投稿中| 🎉 フォロワー1900名突破!🤝 質問大歓迎!みんなで「AIと共に羽ばたく」コミュニティを作りましょう🚀

    あなたへのおすすめ