見出し画像

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

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

いいなと思ったら応援しよう!

YaroTech|生成AIの傾奇者 記事がお役に立てたなら嬉しいです! いただいたチップは、新しいMCPツールの検証や、より深い実践実験の資金として大切に使わせていただきます。 あなたの応援が次の「AI活用の感動」を生み出す原動力になります✨ 一緒に羽ばたき続けましょう!