Web情報取得・要約MCPサーバー



## ソースコード

:::note
[Github](https://github.com/mori-tt/web-information-acquisition-mcp)
:::

## 主な技術とライブラリ等

**@modelcontextprotocol/sdk:** Model Context Protocol (MCP) を実装するためのSDK（ソフトウェア開発キット）です。これを使って、AIモデルや外部ツールと連携するためのMCPサーバー (`McpServer`) を構築しています。`StdioServerTransport` は、標準入出力（stdin/stdout）を使ってMCPクライアントと通信するための方法です。

**@google/generative-ai:** Googleの生成AIモデル（Gemini）を利用するための公式ライブラリです。テキスト生成などのAI機能をサーバーに組み込むために使用しています (`GoogleGenerativeAI`, `GenerativeModel`)。

**express:** Node.jsでWebサーバーやAPIを簡単に構築するための人気のあるフレームワークです。MCPサーバーとは別に、通常のHTTPリクエストを受け付けるためのAPIエンドポイント (`/api/tools/...` など) を作成するのに使われています。

**sitemcp (外部ツール):** このコードから `child_process` を介して呼び出されるコマンドラインツールです。指定されたWebサイトをクロールし、コンテンツを抽出してキャッシュする機能を持っています。Webからの情報収集に使用されます。

:::note 
[sitemcp](https://github.com/ryoppippi/sitemcp)
:::



## プロジェクト構成

リファクタリングにより、コードは機能ごとに以下のファイルに分割されています。

```
.
├── config.ts           # 設定管理、環境変数
├── logger.ts           # ロギング機能
├── types.ts            # TypeScript 型定義
├── repository.ts       # データ永続化層 (JSON ファイル)
├── llmService.ts       # LLM (Gemini) 連携サービス
├── siteDataProvider.ts # Webサイトデータ取得サービス (sitemcp)
├── searchService.ts    # コアビジネスロジック、ハンドラー
├── httpServer.ts       # HTTP サーバー (Express)
├── mcpServer.ts        # MCP サーバー (Stdio)
├── web-mcp.ts          # アプリケーションエントリーポイント
├── package.json
├── tsconfig.json
└── README.md
```

## 主要モジュールの解説

### 1. 設定 (`config.ts`)

-   **役割:** アプリケーション全体の設定値を管理します。
-   **主な機能:**
    -   `.env` ファイルから環境変数 (`GEMINI_API_KEY`, `MCP_SERVER_PORT`, `LOG_LEVEL` 等) を読み込みます。
    -   `GEMINI_API_KEY` が設定されていない場合はエラーをスローします。
    -   `CONFIG` オブジェクトをエクスポートし、HTTP ポート、ログレベル、使用する Gemini モデル名、各種ディレクトリパス (`DB_DIR`, `TMP_DIR`, `ALTERNATIVE_CACHE_DIR`)、タイムアウト値、同時実行数などの設定を定義します。
    -   `setupDirectories` 関数をエクスポートし、起動時に必要なディレクトリ（DB、一時ファイル、キャッシュ）が存在しない場合に作成します。この関数はモジュール読み込み時に自動実行されます。

### 2. ロギング (`logger.ts`)

-   **役割:** 標準化されたログ出力を提供します。
-   **主な機能:**
    -   `Logger` クラスをエクスポートします。
    -   コンストラクタでコンテキスト名（例: "HttpServer", "LlmService"）を受け取ります。
    -   `config.ts` で設定されたログレベル (`LOG_LEVEL`) に基づいて、指定レベル以上のログのみをコンソールに出力します (`debug`, `info`, `warn`, `error`)。
    -   ログメッセージにはタイムスタンプとコンテキスト名が含まれ、デバッグが容易になります。

### 3. 型定義 (`types.ts`)

-   **役割:** プロジェクト全体で共有される TypeScript のインターフェースを定義します。
-   **主な機能:**
    -   `ItemInfo`: 取得・保存する情報の基本構造を定義します（ID, 名前, 組織, 説明, URL, カテゴリ等）。
    -   `ApiSearchRequest`: 検索 API のリクエストパラメータ型。
    -   `ApiResponse`: MCP および HTTP API で共通して使用されるレスポンスの基本構造。`content` (表示用テキスト)、`data` (実際のデータ)、`isError`, `error` (エラー情報) を含みます。
    -   `WebsiteConfig`: `sitemcp` でデータ収集対象とする Web サイトの設定情報（名前, URL, 有効/無効等）。
    -   `PageData`: `sitemcp` が生成するキャッシュファイルのデータ構造（タイトル, コンテンツ, URL）。

### 4. データ永続化 (`repository.ts`)

-   **役割:** データの永続化（ファイルシステムへの JSON 保存）を担当します。
-   **主な機能:**
    -   `ItemRepository` クラスをエクスポートします。
    -   コンストラクタでデータベースディレクトリ (`CONFIG.DB_DIR`) のパスを受け取ります。
    -   `saveItem(item: ItemInfo)`: `ItemInfo` オブジェクトを `db/<item.id>.json` として保存します。
    -   `updateItem(id: string, newData: Omit<ItemInfo, "id">)`: 既存のアイテムを更新します。
    -   `findDuplicateItem(item: Partial<ItemInfo>)`: 名前や組織、URL に基づいて重複する可能性のあるアイテムの ID を検索します（簡易的な重複チェック）。
    -   `getItem(id: string)`: 指定された ID のアイテム情報を取得します。
    -   `getAllItems()`: 保存されているすべてのアイテム情報を取得します。
    -   `getItemsByCategory(category: string)`: 指定されたカテゴリに一致するアイテム情報を取得します。

### 5. LLM 連携 (`llmService.ts`)

-   **役割:** Google Gemini API とのすべての直接的なやり取りを担当します。
-   **主な機能:**
    -   `LlmService` クラスをエクスポートします。
    -   コンストラクタで API キーとモデル名を受け取り、Gemini クライアント (`GenerativeModel`) を初期化します。
    -   `searchWithAI(query: string, category?: string)`: キーワードに基づいて Gemini API に問い合わせ、関連情報を構造化された `ItemInfo[]` として取得します。指定された JSON 形式での応答を要求するプロンプトを使用します。
    -   `generateMarkdownSummary(params)`: `ItemInfo` のリストを受け取り、Gemini API を利用してマークダウン形式の要約記事を生成します。
    -   `generateWebItems(query: string, category?: string)`: Gemini API の Web 検索機能（またはそれに類する機能）を使って直接 Web から情報を収集し、`ItemInfo[]` を生成します。
    -   `extractInfoFromPage(pageData: PageData, ...)`: `sitemcp` が取得した Web ページのデータ（タイトル、コンテンツ）から、指定された JSON 形式に従って `ItemInfo` オブジェクトを抽出します。プロンプトで構造化を指示します。
    -   各メソッド内で API レスポンスのパース（特に JSON）と基本的なエラーハンドリングを行います。

### 6. Web データ収集 (`siteDataProvider.ts`)

-   **役割:** 特定の Web サイトからのデータ収集プロセス（`sitemcp` の実行とキャッシュ処理）を担当します。
-   **主な機能:**
    -   `SiteDataProvider` クラスをエクスポートします。
    -   コンストラクタで関連する設定 (`SiteDataProviderConfig`) と `LlmService` のインスタンスを受け取ります (`LlmService` は取得したページコンテンツから情報を抽出するために必要）。
    -   `searchWebsiteWithSitemcp(website: WebsiteConfig, query: string)`:
        -   対象サイトのキャッシュディレクトリ（標準パスまたは代替パス）を探します。
        -   キャッシュがない場合、`runSitemcpWithTimeout` を呼び出して `sitemcp` を実行し、サイトデータを取得・キャッシュします。タイムアウト処理も含まれます。
        -   キャッシュが見つかった場合、`processWebsiteCache` を呼び出します。
        -   キャッシュが見つからない/生成できない場合、フォールバック設定 (`USE_FALLBACK_FOR_FAILED_SITES`) に応じて `generateSimpleWebItems` を呼び出すか、空の配列を返します。
    -   `runSitemcpWithTimeout(website: WebsiteConfig, altCacheDir: string)`: `npx sitemcp` コマンドを指定された設定（同時実行数、タイムアウト等）で実行する Promise ベースのラッパーです。標準エラー出力などもロギングします。
    -   `processWebsiteCache(cacheDir: string, ...)`: キャッシュディレクトリ内の JSON ファイルを読み込み、`query` に関連するページを見つけ、`LlmService.extractInfoFromPage` を使って情報を抽出します。
    -   `generateSimpleWebItems(website: WebsiteConfig, query: string)`: `sitemcp` が失敗した場合やキャッシュがない場合に、サイト URL を含む基本的なフォールバック情報 (`ItemInfo`) を生成します。

### 7. コアサービス (`searchService.ts`)

-   **役割:** アプリケーションの中心的なビジネスロジックを担い、他のサービス（Repository, LLM, SiteData）を組み合わせて機能を提供します。MCP ツールや HTTP API の具体的な処理（ハンドラー）も実装します。
-   **主な機能:**
    -   `ItemSearchService` クラスをエクスポートします。
    -   コンストラクタで `ItemRepository`, `LlmService`, `SiteDataProvider` のインスタンスを受け取ります（Dependency Injection）。
    -   `searchItems(params)`: 検索の主要なオーケストレーションメソッド。
        -   同時実行数をチェックし、上限に達している場合は `llmService.searchWithAI` のみを呼び出します。
        -   `llmService.searchWithAI` を呼び出して AI ベースの検索結果を取得します。
        -   `useWeb` が true の場合、`searchFromWeb` を呼び出して Web 検索結果を取得します。
        -   結果を統合し、重複をチェック/更新 (`repository.findDuplicateItem`, `repository.updateItem`) します。
        -   カテゴリでフィルタリングし、最終結果を返します。
        -   全体の処理に対するタイムアウト (`CONFIG.SEARCH_TIMEOUT`) を管理します。
    -   `searchFromWeb(query, category)`: Web 検索の内部ヘルパー。
        -   `llmService.generateWebItems` を呼び出して LLM による直接 Web 検索を行います。
        -   `siteDataProvider.searchWebsiteWithSitemcp` を **(TODO: 設定された `TARGET_WEBSITES` リストに対して)** 並行して呼び出し、サイト固有のデータを取得します。 **(現状は `TARGET_WEBSITES` が未解決のためプレースホルダー)**
        -   結果を統合して返します。
    -   `saveItem(itemData)`: 受け取ったデータに ID や作成日時、デフォルトソースを付与し、`repository.saveItem` を呼び出して保存します。
    -   **ハンドラーメソッド (`handleSearchItems`, `handleSaveItem`, `handleGetItemsByCategory`, `handleGenerateMarkdownSummary`)**:
        -   これらは元々 `ItemMCPServer` にあったメソッドで、各ツール/API エンドポイントのリクエストを受け取り、対応するコアロジック（`searchItems`, `saveItem`, `repository.getItemsByCategory`, `llmService.generateMarkdownSummary` 等）を呼び出し、`ApiResponse` 形式で結果を整形して返す役割を持ちます。
        -   入力バリデーション（例: `saveItem` の Zod スキーマ）や基本的なエラーハンドリングもここで行われます。

### 8. HTTP サーバー (`httpServer.ts`)

-   **役割:** Express を使用して HTTP API インターフェースを提供します。
-   **主な機能:**
    -   `HttpServer` クラスをエクスポートします。
    -   コンストラクタでポート番号と `ItemSearchService` インスタンスを受け取ります。
    -   Express アプリケーションを初期化し、ミドルウェア（CORS, `bodyParser.json`）を設定します。
    -   ルート (`/`, `/health`) および API エンドポイント (`/api/tools/*`) を定義します。
    -   各 API エンドポイントは、対応する `ItemSearchService` のハンドラーメソッド (`handleSearchItems` など）を呼び出します。
    -   `createExpressHandler` ヘルパーメソッドで非同期ハンドラーをラップし、共通のエラーハンドリングを行います。
    -   コンストラクタ内で `app.listen()` を呼び出し、HTTP サーバーを指定ポートで起動します。

### 9. MCP サーバー (`mcpServer.ts`)

-   **役割:** `@modelcontextprotocol/sdk` を使用して MCP サーバー（Stdio トランスポート）をセットアップし、ツールを登録します。
-   **主な機能:**
    -   `McpServerWrapper` クラスをエクスポートします。
    -   コンストラクタで `ItemSearchService` インスタンスを受け取ります。
    -   `McpServer` インスタンスを作成します。
    -   `registerTools` メソッド内で、利用可能な各ツール（`search_items`, `save_item` など）を `server.tool()` を使用して登録します。
        -   ツールの名前、説明、パラメータスキーマ（Zod を使用）、そして実行する関数（`ItemSearchService` の対応するハンドラーメソッド）を指定します。
    -   `start` メソッドで `StdioServerTransport` を作成し、`server.connect()` を呼び出して Stdio を介した MCP 通信を開始します。

### 10. エントリーポイント (`web-mcp.ts`)

-   **役割:** アプリケーション全体の起動シーケンスを管理します。
-   **主な機能:**
    -   必要なすべてのモジュール（設定、サービス、サーバーラッパー）をインポートします。
    -   `startServer` 非同期関数内で以下の処理を行います。
        1.  `ItemRepository`, `LlmService`, `SiteDataProvider` を初期化します（設定値や API キーを渡します）。
        2.  `ItemSearchService` を、上記で作成したサービスインスタンスを注入して初期化します。
        3.  `HttpServer` を、設定ポートと `ItemSearchService` を渡して初期化・起動します。
        4.  `McpServerWrapper` を、`ItemSearchService` を渡して初期化し、`start()` メソッドを呼び出して MCP サーバーを起動します。
    -   プロセス終了シグナル (`SIGINT`, `SIGTERM`) を捕捉し、グレースフルシャットダウン処理（現在は `process.exit(0)` のみ）を呼び出します。
    -   `uncaughtException` や `unhandledRejection` に対するグローバルなエラーハンドリングを設定し、予期せぬエラーでプロセスが終了するようにします。
    -   最後に `startServer()` を呼び出してアプリケーションを起動します。

## 主要ワークフローの例

### `search_items` の流れ

1.  **クライアント (HTTP or MCP):** `search_items` ツール/API を呼び出し、`query`, `category`, `useWeb` パラメータを送信。
2.  **`httpServer.ts` or `mcpServer.ts`:** リクエストを受け取り、バリデーション後、`ItemSearchService.handleSearchItems` を呼び出す。
3.  **`searchService.ts` (`handleSearchItems`):**
    -   `searchItems` メソッドを呼び出す。
4.  **`searchService.ts` (`searchItems`):**
    -   同時実行数をチェック。
    -   `LlmService.searchWithAI` を呼び出し、AI ベースの結果を取得。
    -   `useWeb` が true の場合:
        -   `searchFromWeb` を呼び出す。
        -   **`searchService.ts` (`searchFromWeb`):**
            -   `LlmService.generateWebItems` を呼び出し、LLM 直接検索結果を取得。
            -   **(`TARGET_WEBSITES` リストに基づき)** `SiteDataProvider.searchWebsiteWithSitemcp` を各サイトに対して呼び出す。
            -   **`siteDataProvider.ts` (`searchWebsiteWithSitemcp`):**
                -   キャッシュ確認。なければ `sitemcp` 実行 (`runSitemcpWithTimeout`)。
                -   キャッシュ処理 (`processWebsiteCache`)。
                -   **`siteDataProvider.ts` (`processWebsiteCache`):**
                    -   該当ページが見つかれば `LlmService.extractInfoFromPage` を呼び出し情報抽出。
            -   結果を統合して返す。
        -   AI 結果と Web 結果を統合・重複処理 (`repository.findDuplicateItem`, `updateItem`)。
    -   カテゴリでフィルタリング。
    -   タイムアウト処理。
    -   最終結果を `handleSearchItems` に返す。
5.  **`searchService.ts` (`handleSearchItems`):** 結果を `ApiResponse` 形式に整形。
6.  **`httpServer.ts` or `mcpServer.ts`:** 整形されたレスポンスをクライアントに返す。

### `generate_markdown_summary` の流れ

1.  **クライアント (HTTP or MCP):** `generate_markdown_summary` ツール/API を呼び出し、`title`, `category` 等のパラメータを送信。
2.  **`httpServer.ts` or `mcpServer.ts`:** リクエストを受け取り、`ItemSearchService.handleGenerateMarkdownSummary` を呼び出す。
3.  **`searchService.ts` (`handleGenerateMarkdownSummary`):**
    -   `category` に基づいて `ItemRepository.getItemsByCategory` または `getAllItems` を呼び出し、対象のアイテムリストを取得。
    -   取得したアイテムリストとパラメータを `LlmService.generateMarkdownSummary` に渡す。
    -   **`llmService.ts` (`generateMarkdownSummary`):**
        -   アイテムリストを JSON 文字列化。
        -   適切なプロンプトを生成し、Gemini API を呼び出す。
        -   結果のマークダウンテキストを返す。
    -   結果を `ApiResponse` 形式に整形。
4.  **`httpServer.ts` or `mcpServer.ts`:** 整形されたレスポンスをクライアントに返す。