Search that shows its work について
searchは、検索結果の出典や信頼性を確認したいエージェント向けのセルフホスト型ウェブ検索MCPサーバーです。管理下のメタ検索インスタンスを使い、検索やエンジンの網羅性、読み取りの問題を判定可能なフィールドで返します。
主な機能
- 01ウェブ検索と上位ページの読み取り
- 02ウェブページ、PDF、スキャン画像の読み取り
- 03画像検索とページのスクリーンショット
- 04複数段階での検索と情報源の集約
- 05問題や検索範囲を確認できるデータ
活用シーン
- 01複数の検索エンジンに結果がまたがるか確認
- 02JavaScript描画ページやPDFを読む
- 03スクリーンショットとページ本文を照合
- 04未確認箇所を示しながら複数の情報源を集約
MCP 対応
ウェブ検索、読み取り、画像検索、スクリーンショット、深層検索のMCPツールを公開します。リモートHTTPまたはstdioで接続でき、検索には設定済みのメタ検索サービスが必要です。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
検索ツールの失敗は、成功したときとまったく同じように見えることがあります。検索エンジンが、別の人のテーマについて回答する。ページが、テキストの代わりにボット対策の盾を返す。16 個の情報源が、実際には 2 つの検索エンジンを 8 回ずつ数えたものだった。こうしたことが起きてもエラーは発生せず、向こう側のモデルはその結果をもとに推論を進めてしまいます。
**このサーバーの役割は、こうしたケースを判別できるようにし、分岐処理に使えるフィールドで示すことです。**自分のマシン上で、自分のメタ検索インスタンスを介し、自分のモデルキーを使って(またはキーなしで)実行できます。
🔧 ツール
| ツール | 機能 | 必須 | 主なオプション |
|---|---|---|---|
web_search | ページを見つけ、上位のページを読み込みます — 1 回の呼び出しで、リンクとそのテキストを返します | query | リンクのみの場合は read: false · 読み込む件数は read_top · 幅広く検索するには min_engines · 検索エンジンを自分で指定するには engines |
web_read | アドレスを指定してページを読み込みます:テキスト、PDF、またはビジョンモデルが認識したスキャン画像 | urls | JavaScript で描画されるページには mode: browser · 必須コンテンツを検証するには expect · 続きから読み込むには offset |
web_image_search | 画像を見つけます:ファイルのアドレスと、その画像が掲載されているページのアドレスを別々に返します | query | max_results、page |
web_screenshot | ページの PNG と、同じアクセスで取得したテキストを返すため、両者を照合できます | url | テキストの最大文字数は max_chars · full_page · expect |
web_deep_search | クエリを自ら組み立て、複数のウェーブに分けて読み込み、複数の情報源から回答します — 確認できなかった内容も示します | question | waves |
引数、レスポンス形式、失敗モードの完全なリファレンス:HOWTO-CALL.md。
📦 インストール
公開されているリポジトリページから、回答を返せる状態まで。あらかじめディスク上に用意しておくものはありません。
git clone https://github.com/AG-Bureau/mcp-search
cd mcp-search
cp .env.example .env
echo "SEARXNG_SECRET=$(openssl rand -hex 32)" >> .env
echo "BROWSER_WS_SECRET=$(openssl rand -hex 16)" >> .env
echo '{}' > door-tokens.json && mkdir -p tls
docker compose -f docker-compose.yml -f wiring/expose-localhost.yml up -d --build
curl -s http://127.0.0.1:8081/healthz
4 行目と 5 行目は飾りではありません。SEARXNG_SECRET に値がないと、up コマンドは実行を拒否します。これは意図的な動作です。独自のキーがない場合、メタ検索は失敗するのではなく、イメージのテンプレートに含まれる公開済みのキーで、何も通知せずに起動してしまうためです。BROWSER_WS_SECRET は、ブラウザーサイドカーのエンドポイントにあるシークレットパスです。これがない場合も up は実行を拒否します。推測可能なパスでは、サイドカーへの WebSocket 接続を開けるものなら何でも(compose ネットワーク上の任意のコンテナーや、ブラウザー自身が開く任意のページも含めて)、モジュールが読み込むページに適用するすべてのチェックを迂回して、自分専用のブラウザーを使えてしまうためです。compose ファイルは同じ値をサイドカーと、BROWSER_WS_URL の末尾にあるアダプターに渡します。どちらもその値を表示しません。
6 行目では、呼び出し元の認証と TLS 用に compose ファイルがマウントする 2 つのパスを作成します。どちらもデフォルトでは無効です。存在しないパスは Docker によって root 所有のディレクトリとして作成されます。また、ディレクトリになったトークン「ファイル」は、読み取り不能な一覧として読み込まれます(下記の「設定」を参照)。
どちらも .gitignore に含まれています。使用するとシークレットが保存されるためです。
オーバーレイは、ポートをループバックでのみ公開します。公開されたコンテナーポートは、通常のホストファイアウォールのチェーンを経由しないため、より広い範囲に公開するには、別途意図的な設定が必要です。デプロイと公開を参照してください。
2 つのトランスポート
MCP には 2 つのトランスポートがあり、それぞれ異なる用途に対応します。HTTP — 上記のコマンド — は、すでにどこかで起動しているサーバー向けです。stdio はプロトコルの デフォルトです。クライアントがサーバーをプロセスとして起動し、パイプ経由で通信します。 ほとんどのデスクトップクライアントやラッパーはこの方式で動作します。
docker compose exec -T ag-search python /app/server.py --stdio # or MCP_TRANSPORT=stdio
# outside compose: SEARXNG_URL=http://<a metasearch you can reach> python adapter/server.py --stdio
入力は 1 行につき JSON-RPC オブジェクト 1 つ、出力は 1 行につき応答 1 つです。モードは 明示的に選択し、ターミナルが接続されているかどうかから推測することはありません。その印は 対象の隣にあるだけで、いつか誰も意図していないケースにまで答えてしまうからです。
stdio モードでは、stdout がプロトコルです。応答だけを出力し、ログは stderr に出力します。 それ以外の行が 1 行でも紛れ込むと、それを読み取るクライアントが動作しなくなります。
この方法で起動した各プロセスは、実行中のコンテナ内でも独立したインスタンスです。
ブラウザーセッション(BROWSER_MAX_SESSIONS)、エンジンとドメインのペーシング、
/stats カウンターは、HTTP サーバーのものとは別です。同じサイドカー上の複数の stdio クライアントは、
複数のアダプターと同様に pids_limit に加算されます。
サイドカーはどちらの選択にも依存しませんが、メタサーチは依存します。SEARXNG_URL
がサーバーのいずれかに到達できる必要があり、そうでなければすべての検索が
cause: upstream で拒否されます(デフォルトの
http://searxng:8080 は compose ネットワーク内でのみ認識される名前です)。compose プロジェクトがない状態で
クライアントから起動した場合、モジュールは不足しているものを明示し、あるかのようには振る舞いません。
ブラウザーパスは not_wired_up を返し、trouble には
pool_unmeasured が含まれます。エンジンプールは観測結果から計算されていません。
⬆️ 0.3.5 へのアップグレード — 新しい変数はありません
0.3.4 からのアップグレードでは、インストール手順と同じコマンドを、同じ -f ファイルで実行します。
変更されるのはアダプターイメージのみです(アダプターとプローバーで使用する
ag-mod-search/adapter:0.3.5)。サイドカーは
ag-mod-search/browser:1.49.1-3 のままです。気づく可能性がある変更は 2 つあります。
ドアがリクエストごとに 1 行ログを出力するようになりました(呼び出し元のアドレス、パス、ステータス、
呼び出し元のトークン名を記録します。トークン、本文、クエリー文字列は記録しません)。
また、呼び出し元に割り当てられた時間を超過したモデル呼び出しは、そのまま実行を続ける代わりに、
その旨を伝える拒否を返すようになりました。0.3.3 以前からアップグレードする場合は、
以下の 0.3.4 の手順も適用されます。
⬆️ 0.3.4 へのアップグレード — 新しい変数はありませんが、制約が 1 つ厳しくなります
0.3.3 からのアップグレードでは、インストール手順と同じコマンドを、同じ -f ファイルで実行します。
変更されるのはアダプターイメージのみです。サイドカーは
ag-mod-search/browser:1.49.1-3 のままです。まず AG_DOOR_AUTH を確認してください。
値が off、warn、on のいずれでもない場合(大文字・小文字は区別せず、前後の空白は無視されます)、
以前は off として扱われましたが、今後はアダプターの起動が停止します。0.3.2 以前から
アップグレードする場合は、以下の 0.3.3 の手順も適用されます。
⬆️ 0.3.3 へのアップグレード — 新しい必須変数が 1 つあります
up の前に .env に BROWSER_WS_SECRET を設定する必要があります。インストール手順と同様に、
openssl rand -hex 16 を使って追加してください。その後、インストール手順のコマンドを、
インストール時と同じ -f ファイルで実行します。
docker compose -f docker-compose.yml -f wiring/expose-localhost.yml up -d --build。
イメージまたは環境が変更されたため、両方のイメージを再ビルドし、アダプター、プローバー、
サイドカーを再作成します。サービス名は指定しないでください。up … browser ag-search とすると、
プローバーは古いイメージのままになります。また、-f オーバーレイを省略すると、
公開ポートのない状態でアダプターが再作成されます。サイドカーイメージは
ag-mod-search/browser:1.49.1-3 です。古いサイドカーを使っている場合や、
いずれか 1 つしか再作成していない場合、ブラウザーパスは「not responding」と表示されます。
形式が正しくないシークレットがあると、モジュール全体が停止します。 Compose が確認するのは、
値が空でないことだけです。A-Z a-z 0-9 _ - 以外の文字(base64 では + / = が含まれます)があると、
サイドカーは 78 で終了して再起動を繰り返します。アダプターは正常なサイドカーを待つため、
起動せず、up は「dependency failed to start」と表示します。
ドアを warn または on で実行する場合は、Configuration の「Moving to on」も読んでください。
プローバーがトークンを必要とするようになりました。スコープのないトークン(または scopes が
文字列として記述されたトークン)では、どのツールも呼び出せない場合があります。また、
/healthz?deep=1 には、検索または読み取りが可能なトークンが必要です。
⬆️ 0.2.x からのアップグレード — 回答の形式が変わりました
すでに 0.2.1 以前を実行している場合は、更新前にお読みください。 ここに記載するのは、無視してもよい新機能ではありません。既存の呼び出しが返す内容の変更点です。
検索と画像の回答では、デフォルトで返されるフィールドが減り、名前にもその変更が反映されます。 /2 ではなく、ag.search/3 と ag.images/3 になります。contract の値に応じて処理を分岐していた呼び出し元は、明確にエラーになります。ほとんどの言語では、フィールドが単に消えると「何も問題はなかった」と解釈されてしまうため、意図的な変更です。
| 0.2.x での項目 | 0.3 での項目 |
|---|---|
search_aborted, engines_unasked | trouble.search_aborted, trouble.engines_unasked |
unresponsive_engines | trouble.unresponsive_engines |
engines_irrelevant | trouble.engines_irrelevant |
pool_source: "seed" | 理由を含む trouble.pool_unmeasured |
arguments_adjusted | trouble.arguments_adjusted |
corroborated_by_url, corroborated_by_domain — 常に存在し、軽量処理では 1 | 検索結果を見つけたエンジンが1つだけの場合は省略。この場合の 1 は測定値ではなく、「ほかのエンジンには問い合わせていない」という意味でした |
count, query, page, read, read_top, pages_*, timing_ms, engines_skipped, engines_used, tiers_used, pool_source, pool_reason | verbose: true を指定した場合に返されます |
問題がなければ trouble は空の状態で常に存在するため、4つの個別チェックを if not trouble に置き換えられます。モジュールから何かが削除されたわけではありません。計測情報がリクエストを指定した場合にのみ返されるようになりました。
ブール値はキャストではなく、パースされるようになりました。 0.2.x では、MCP 経由で "read": "false" を指定してもページが読み込まれていました。処理時間が8倍になっていた一方、通常の入口では同じ値が正しく解釈されていました。どちらの入口も、大文字と小文字を区別せず、true/false、1/0、yes/no、on/off、y/n、t/f と Python の True/False を受け付けるようになりました。解釈できない値はフラグをオフにし、arguments_adjusted に記録されます。 EMPTY 値も解釈できない値として扱われるため、read="" を指定しても、高コストなデフォルト値が黙って適用されることはありません。
read_top: 0 は「指定なし」を意味し、「何も読まない」ではありません。 何も読まないようにするには、read: false を使用してください。以前はスキーマで minimum: 1 と宣言しながら、ゼロを受け付けていました。
フィールドごとの表は、コントラクトの contracts/ag.search.v3.md と
contracts/ag.images.v3.md の「What changed
against /2」セクションにあります。
読み取り(ag.read/2)、ディープ検索(ag.deep/2)、スクリーンショット(ag.shot/1)の番号は変わっていません。フィールドが追加されましたが、追加によって既存の呼び出しが壊れることはありません。
⚙️ 設定
| 変数 | 必須 | 内容 |
|---|---|---|
SEARXNG_SECRET | はい | メタ検索のセッションキーです。誰かの履歴に由来しない、長いランダムな文字列を指定してください。 |
BROWSER_WS_SECRET | はい | ブラウザーサイドカーのシークレットエンドポイントのパスです。A-Z a-z 0-9 _ - の文字を16~256文字指定します(openssl rand -hex 16)。指定しないとサイドカーは起動時に終了し、compose ファイルも先に拒否します。これ以外の文字を含む値は compose ファイルを通過しますが、モジュール全体が停止します。サイドカーは終了コード78で終了し、正常なサイドカーを待つアダプターも起動しません。ローテーションするには値を変更し、インストール時と同じ -f ファイルを指定して、同じ up -d を実行します。両方の環境が変更されるため、browser と ag-search が再作成されます。 |
LLM_API_KEY | いいえ | OpenAI 互換のエンドポイント用のキーです。シークレットです。 |
LLM_API_BASE | いいえ | そのエンドポイントのベース URL です。類推で推測せず、プロバイダーのドキュメントから取得してください。単純に推測した URL では、429: Insufficient balance のような課金エラーが返る場合があります。一部のプロバイダーは、同じドメインでも別のパスでサブスクリプションを提供しているためです。 |
LLM_MODEL_TEXT | いいえ | クエリの計画と回答の作成に使うモデルです。デフォルト値はありません。デフォルトを設定すると、プロバイダーが利用できないモデルを気付かないままリクエストする可能性があるためです。 |
LLM_MODEL_VISION | いいえ | スキャンした PDF の読み取りに使うモデルです。未設定の場合、そのようなドキュメントには理由を明示して拒否を返します。 |
LLM_DISABLE_THINKING | いいえ | 推論用の予算が回答を使い切り、finish_reason: length で回答が空になるプロバイダーでは設定してください。 |
LLM_MAX_CONCURRENT | いいえ | 同時に実行できるモデル呼び出しの数です。プロバイダーに拒否されるのではなく、こちらで制限します(デフォルトは20)。プロバイダーは上限を超えた呼び出しを拒否しますが、その時点ではクエリの作成とページの読み取りが済んでおり、最もコストがかかっています。最初に順番待ちをすれば、一時停止するだけで済みます。0.3.5以降、待機時間には上限があります。呼び出しが時間内に空きスロットを見つけられない場合、モデルを呼び出さずにこちらで拒否し(no free model slot)、スキャンは not_recognised、ディープサーチは busy を返します。プロバイダーが示す上限より小さい値を設定してください。プロバイダーはアカウント全体で数えるため、1つのキーを複数のインスタンスで使うことがよくあります。 |
LLM_RATE_LIMIT_WAIT_S | いいえ | プロバイダーが、利用可能な Retry-After を付けずに429を返したときの待機時間です。Retry-After がない、0、負の値、または日付の場合に適用されます(デフォルトは5、最小値は1)。0秒後の再試行は、同じリクエストをもう一度送るだけです。 |
LLM_RATE_LIMIT_CODES | いいえ | HTTP 429以外で「リクエスト過多」を意味する、プロバイダー独自のエラーコードをカンマ区切りで指定します。エラー本文内の error.code(またはトップレベルの code)と完全一致で比較します。本文中の文言を検索することはありません。空の場合のデフォルトでは429のみが該当します。すべての429が該当します。課金拒否を429として返すプロバイダーの場合も、error にプロバイダーの文言を含めて busy と報告されます。 |
DEEP_RATE_LIMIT_MAX_WAIT_S | いいえ | レート制限後の1回の再試行で、ディープサーチが待機する最長時間です(デフォルトは10)。これを超える待機が必要な場合は、代わりに cause: busy を返します。 |
AG_DOOR_AUTH | いいえ | 呼び出しを許可する方法です。off(デフォルト。チェックなし。このモジュールが常に使っていた動作)、warn(ツールを提供しますが、有効なトークンなしで届いた呼び出しをカウントします。ただし /healthz?deep=1 は例外で、後述のとおり warn でも拒否します)、on(トークンなしなら401、そのツールを呼び出す権限がないトークンなら403で拒否します)。大文字・小文字と前後の空白は区別されません。未設定または空の場合は off です。それ以外の値を指定すると、サーバーの起動が停止します。エラーメッセージには指定された値と3つの許可値が表示されます。0.3.4より前は、enforce や 0n のような入力ミスが黙って off として扱われていました。off → warn → on の順に切り替えてください。最初から on にすると、まだトークンを渡していない呼び出し元が締め出されます。 |
AG_DOOR_TOKENS_IN_CONTAINER | いいえ | コンテナ内でのトークン一覧のパスです。compose ファイルは ./door-tokens.json を /run/door-tokens.json にマウントするため、/run/door-tokens.json を設定してください。未設定の場合、トークン一覧は読み込まれません。ファイルの形式は {"<token>": {"name": "...", "scopes": ["read"]}} です。**シークレットであり、git に追加しないでください。**スコープには、ツール名、グループの read と paid、または * を指定できます。スコープ一覧がない、または空の場合は何も許可されません。また、scopes が一覧ではなく文字列の場合も何も許可されません。一覧内で明示的に "*" を指定した場合にのみ、すべてが許可されます。/stats では、このようなトークンが tokens_granting_nothing の下に名前で表示されます(文字列の場合は tokens_scopes_not_a_list の下にも表示されます)。paid が許可するのは web_deep_search です。また、スキャンした PDF に対して web_read がビジョンモデルを使うには、このスコープが必要です。on では paid を持たないトークンでも読み取りはできますが、スキャン結果は、不足しているスコープを示す not_recognised (scan) になります。ドアはファイルから名前とスコープを読み込み、トークン自体を出力することはありません。拒否時もカウンターにも出力されません。通常の /healthz はどのモードでも開放されています。実際の検索と読み取りを行う /healthz?deep=1 には、warn と on で web_search または web_read を呼び出せるトークンが必要です(トークンなしなら401、どちらも呼び出せないトークンなら403)。 |
PROBE_DOOR_TOKEN | いいえ | ドア用のプローバーのトークンです。トークン一覧から web_read スコープのみを持つものを指定してください。warn を有効にする前に必要です。これがないと、読み取りプローブのたびに unauthenticated がカウントされ、そのカウンターが増え続けます。on ではドアがプローブを拒否します。その場合、プローバーはプローブの記録を残さず、ログに理由を記録します。空の場合はトークンを送信しません。シークレットです。.env のみに記載してください。 |
AG_DOOR_TLS_CERT、AG_DOOR_TLS_KEY、AG_DOOR_TLS_PORT | いいえ | 通常のポートとは別に、専用ポートで TLS を有効にします。コンテナ内のパスを指定します。compose ファイルは ./tls を /run/tls にマウントします。さらに、待ち受けるポートも指定してください。3つすべてを設定するか、どれも設定しないでください。2つだけ設定した場合や、証明書を読み込めない場合は、ドアの起動が停止します。メインポートは常に平文のままなので、compose のヘルスチェックは引き続き動作します。wiring/expose-localhost.yml は TLS ポートを公開しません。 |
READ_CONTACT | いいえ | ページを取得するときに User-Agent に設定する連絡先です。デフォルトではこのリポジトリが設定されます。このモジュールを大規模に運用する場合は、独自の値を設定してください。 |
READ_LANGUAGES | いいえ | 読み取り時の Accept-Language です。デフォルトでは未設定です。読み取るページの言語は、こちらで決めるものではありません。 |
READできないトークンファイルは、空のトークンファイルではありません。 実際のデプロイで見られた事例です。コンテナは非特権ユーザーとして実行されているのに、ファイルは root が所有するモード 600 でコンテナにマウントされていました。ドアは何も開かず、拒否もせず、正しく設定されているように見えました――見ることができないという状態が、見た結果として返されていたのです。そのため /stats には、個別のフィールドが3つあります。door_auth(モード)、tokens_configured(ファイル名が指定されているか)、tokens_readable(トークンを1つ以上読み取れたか。読み取り可能な {} も false になります)。読み取れないリストは on では全員を拒否します――読み取れなかったファイルに対する安全な動作です――一方、warn では全員にサービスを提供します。まさにこの違いがあるため、2つのケースを同じフィールドで扱ってはいけません。パスが存在しない場合はさらに厄介です。Docker はそのパスに DIRECTORY を作成し、読み取り不能なファイルのように見えてしまいます。そのため、最初の up の前にファイルを作成し(上記のインストール手順では作成します)、それがファイルであることを確認してください。 |
シークレットファイルの所有者について。 アダプターは uid 10001 で実行されます。トークンファイルにトークンを格納したら、また TLS キーについても、次を実行します。chown 10001:10001 door-tokens.json tls/key.pem && chmod 600 door-tokens.json tls/key.pem。上記の失敗に対する一見簡単な解決策である chmod 644 を使うと、ホスト上のすべてのユーザーがトークンと秘密鍵を読み取れるようになります。
on への切り替え。 まずプローバーにトークンを設定し(PROBE_DOOR_TOKEN)、off → warn と進めてから、/stats の unauthenticated が増えなくなった時点で on に切り替えてください。プローバーにトークンを設定した状態でそこに残っているのは、まだトークンを渡していない呼び出し元です。0.3.5 以降、ログには呼び出し元が表示されます。リクエストごとに1行、[door] <address> <method> <path> -> <status> in <ms> ms, caller: <token name> と出力されます(トークンがない場合は no token、トークンリストにないトークンの場合は unknown token です。呼び出し元への拒否応答では両者を区別しませんが、ログでは区別します。ドアによる判定が行われなかった場合は - です。たとえば、読み取り前に拒否されたリクエスト、HEAD や OPTIONS などドアが受け付けないメソッド、/mcp 以外への POST が該当します)。リクエストにクエリ文字列があった場合は ?… が付き、クライアントが送信した場合は、検証されていないクライアント自身の X-Forwarded-For も表示されます。トークン、その他のヘッダー、本文、クエリ文字列はログに記録されません。
ブラウザーのセッションとウォッチドッグの制限は .env で設定します。各設定項目の説明は .env.example を参照してください。ペーシング、プールサイズ、読み取り制限には adapter/server.py、adapter/reader.py、adapter/pool.py に実測に基づくデフォルト値があります。docker-compose.yml はこれらを引き渡しません。
モデルキーは任意です。 検索、読み取り、画像検索、スクリーンショットは HTTP リクエストであり、モデルのトークンを消費しません。モデルが呼び出される箇所は2つだけで、どちらも回答に明記されます。web_deep_search と、テキストレイヤーのない PDF の認識です。後者は、そのような文書の読み取りを依頼したときに発生し、web_search が裏で勝手に行うことはありません。web_deep_search は選択したページを認識を有効にして読み取るため、その中にスキャン画像があれば、その検索に伴うモデル使用量の一部として認識されます。ドアが on の場合、どちらにも paid スコープのトークンが必要です。
🎯 バンドルされた検索ツールではできないこと
コストを制御できます。 1つの引数を変えるだけで、回答の規模は桁違いになります。
| 呼び出し | ペイロード | 時間 | モデルのトークン数 |
|---|---|---|---|
read: false、リンク6件 | 3.8 KB | 0.6 s | 0 |
read_top: 1、リンク3件 | 8.8 KB | 1.6 s | 0 |
read_top: 3、リンク6件 | 9.8 KB | 6.3 s | 0 |
web_deep_search | アカウント全体 | 36 s | 6回の呼び出し |
1台のマシンで1つのクエリを測定した値です。数値ではなく傾向を参考にしてください。 ある利用者が同じフォークを外部から測定したところ、0.7 sに対して4.8-10.5 sという結果でした。傾向は変わりませんが、数値はクエリが見つけるページによって異なります。
選択は請求が届いてからではなく、呼び出す前に行います。 何があるかを把握するときや、コンテキストの上限が厳しいときはread: falseを使います。上位の結果のテキストが必要で、通常なら自分で取得する場合は、デフォルトの設定を使います。read_top: 0は「希望なし」という意味であり、「何も読まない」という意味ではありません。
エンジンのリストは自動で保守されます。 手書きのリストは、気づかないうちに古くなります。以前は最適だったエンジンが数週間後に何も返さなくなっても、そのことを知らせてくれません。このリストは1日で3回改訂されました。各改訂は前のリストを基に行われ、それぞれの時点のデータに対して正しいものでした。問題はエンジンではありません。観測が続く一方で、判断が固定されてしまうことです。
そのため、リストはここには記載していません。プローブが、正解があらかじめ分かっている質問を使って、既知のすべてのエンジンに継続的に問い合わせます。エンジンプールには、参照データへのヒット率が最も高い少数のエンジンが選ばれ、自動で再計算されます。反証による検証も行っています。故意に不正な実行結果を混ぜたところ、コードを変更せずにエンジンがプールから外れ、実行結果を元に戻すと自動で復帰しました。
十分な観測データが集まるまでは、プールはシードリストであり、すべての回答にその旨が示されます。デフォルトではtrouble.pool_unmeasured、verboseではpool_source自体が表示されます。
失敗と成功を区別できます。 エンジンの失敗には4つのパターンがあり、それぞれ次のように確認できます。
| 失敗の仕方 | 表示される内容 |
|---|---|
| 拒否を返す:CAPTCHA、レート制限、BAN | trouble.unresponsive_engines |
| 何も返さずに応答を終える | engines_askedとengines_answeredの差 |
| 別の質問に答える | trouble.engines_irrelevant — 結果はすでに破棄されています |
| 対象を、よりインデックス化されている同名の別対象に置き換える | 参照データに対する評価で得られるengines_trust |
読み取りにも同じ考え方が当てはまります。結果は7種類に分かれ、遮断ページが返された場合は、空のテキストではなくstubとして扱われます。
🩺 モジュールが停止し、異常が見当たらない場合
このシステムでは、通常監視する項目には現れない障害が1つあります。再発時に原因を一から調べ直すことのないよう、ここに記録しておきます。
症状。 コンテナが異常と報告され、docker compose upでアダプターの起動が拒否され、docker execはrunc: nsexec ... setnsで失敗します。負荷、メモリ、ディスクには問題がありません。
確認コマンドは1つです。
ps -eo comm | sort | uniq -c | sort -rn | head
headless_shellが数千件に達している場合、ブラウザーのサイドカーにプロセスが蓄積しています。開いたまま閉じられなかったブラウザーはレンダラープロセスを残し、数千件を超えるとコンテナランタイムがプロセスを起動できなくなります。ヘルスチェックはどちらもexecで動作するため、その時点で一斉に異常を報告し、直近で変更したものが原因のように見えてしまいます。
解決策: docker compose up -d --force-recreate browser。自分たちのインスタンスで測定したところ、プロセス数は実行前の4619から126になりました。
0.3.1での変更点。 プロセスは実際には動作中ではありませんでした。訪問が終わるとChromiumの子プロセスは孤立し、wait()を呼び出さないPID 1によって、各プロセスはスロットを占有したゾンビとして残ります。測定では10回の訪問で40件に達しました。サイドカーのイメージは、プロセスを回収する正式なinitを実行するようになりました(イメージを置き換える場合も、composeファイルのinit: trueで同じ動作を指定できます)。また、モジュールは失敗した場合も含め、すべての経路でブラウザーを閉じるようになりました。
2つ目のサイレント障害:すべてのブラウザー読み取りがハングし、その後ディスクがいっぱいになる
症状。 ブラウザーを使う web_read と /healthz 自体が応答を停止する一方で、検索は引き続き動作します。複数のアダプターが1つのサイドカーを共有している場合は、すべてのアダプターでブラウザー読み取りが失敗します。その後、ログが増えないままマシンのディスクがいっぱいになります。
確認方法 — プロセス数ではなくスレッド数を数えます:
docker stats --no-stream --format '{{.Name}} {{.PIDs}}' | grep browser
pids_limit はコンテナー内のすべてのスレッドを数えます。アイドル状態の Chromium ツリーは約42スレッド、run-server 自体のベースは約14スレッドです。上限に達すると、新しいブラウザーはスレッドを作成できずにクラッシュしますが、サイドカー独自のヘルスチェックである TCP 接続は正常なままです。ps には少数のプロセスが表示されるだけで、異常は見つかりません。
0.3.2で変更された内容は、2026-09-23にまさにこの障害が発生した後のものです。同時読み取りの急増で上限に達し、タイムアウトのない段階で11個のセッションが6時間ハングしました。ヘルスプローブはロック待ちでハングし、クラッシュした Chromium はコンテナーレイヤーにコアダンプを書き込み続け、最終的にディスクがいっぱいになりました。
- すべてのブラウザーセッションはそれぞれ独立した子プロセスで実行され、予算時間に
BROWSER_KILL_GRACE_S(デフォルト10秒)を加えた時間を超えると、プロセスグループごと強制終了されます。呼び出し元にはその旨を示すnot_reachedが返され、原因がサイドカーかページかも通知されます。スクリプトが一度も処理を返さないページに問題がある場合、それはページ側の責任であり、サイドカーは異常とは判定されません。 - 1つのアダプターインスタンスが同時に保持できるセッション数は最大
BROWSER_MAX_SESSIONS(デフォルト2、ヘルスプローブを含む)です。これを超える読み取りは、サイドカーが実行できないブラウザーを起動するのではなく、最大BROWSER_SLOT_WAIT_S(デフォルト5秒)待った後、ブラウザーがビジーであることを通知します。待機時間はセッションの持ち時間から差し引かれ、予算時間は呼び出しに残された時間まで短縮されます。それでもハングするセッションのみ、その後BROWSER_KILL_GRACE_S(デフォルト10秒)で強制終了されます。ぎりぎりで応答したセッションには終了まで最大3秒の追加時間が与えられるため、デフォルト設定では呼び出しが上限時間を最大13秒超過する場合があります。また、セッションがサイドカー上にブラウザーを残した可能性があるスロット(起動中に強制終了された場合や、終了処理がハングした場合)は、サイドカーのウォッチドッグがクリーンアップするまでBROWSER_COOLDOWN_S(デフォルト150秒)の間、再利用されません。 /healthzはブラウザーを起動せず、ブラウザーを待つこともありません。ブラウザーの状態には、直近の実セッションの結果とその経過時間、およびポートチェックが反映されます。実際のプローブはバックグラウンドでBROWSER_PROBE_S(デフォルト600秒)ごとに最大1回実行されます。直近の判定が「応答なし」の場合は、各ヘルスチェック時に実行されるため、復旧は10分ではなく数分で反映されます。起動後に最初のプローブが実行されるまではnot checked yet: …と表示され、状態は degraded と判定されます。- サイドカーのメインコマンドはウォッチドッグ(
browser/supervise.py)です。クライアントの動作にかかわらず、BROWSER_MAX_AGE_S(デフォルト120秒。正当なセッションの最長時間は42.5秒)を超えた Chromium ブラウザーを強制終了します。 - アダプター、プローバー、ブラウザーでは
ulimits: core: 0を設定しているため、いずれもコアダンプを書き込みません。また、アダプターのイメージには独自の init が含まれています。
それでも発生した場合の対処法: docker compose up -d --force-recreate browser を実行します。アダプター側の対応は不要です。0.3.2以降では /healthz が応答し続け、セッションスロットが空き次第、ブラウザーの状態は自動的に alive に戻ります。サイドカーが応答しない間に強制終了されたセッションのスロットは BROWSER_COOLDOWN_S(150秒)の間、再利用されないため、復旧には最大で約3分かかります。テスト環境での計測では、サイドカーの再作成から alive に戻るまで167秒かかり、その間に読み取りは行いませんでした。GET /pages はスロットが空き次第、実際のブラウザーセッションを実行します。より早く対応する必要がある場合は、待機を省略して docker compose restart ag-search を実行できます。例外は0.3.2より古いアダプターです。この場合、/healthz がハングする可能性があるため、docker compose up -d --force-recreate ag-search を実行してアップグレードする必要があります。
⚖️ robots.txt に対する動作と、判断が必要な理由
このモジュールはサイトのルールを報告しますが、適用はしません。 すべての読み取り結果には robots が含まれ、その値は allowed、disallowed_by_site、またはファイルを読み取れなかった場合の not_checked です。サイトが禁止しているページも取得され、その旨が回答に示されます。
allowed は、ルールを読み取り、このパスが許可されていることを意味します — 単に禁止されていないことを確認しただけではありません。サイトがファイルは存在しないと示す(404)場合、ルールはないため、これは allowed です。ファイルがまったく届かなかった場合 — 5xx、タイムアウト、接続の切断、401/403 — や、届いたファイルを解釈できないエンコーディングだった場合は not_checked です。これらは異なる事象であり、このフィールドでは区別されます。データがなければ「未確認」であり、「許可」ではありません。
これは省略ではなく判断であり、モジュールではなく、これを実行する側が決めることです。理由は2つあります。人が呼び出すツールに robots.txt を遵守させるかどうかは、運用者の判断です。ウェブをインデックス化するクローラー向けに書かれたルールが、ユーザーの依頼した1ページを取得する場合にも適用されるとは限りません。また、参照動作のように robots.txt に対する 401/403 を禁止として扱うと、通常の情報源を誤って禁止と判定します。アンチボットサービスの背後にある同じサイトが、誰に対しても 401 を返すことがあるためです。これを禁止とは呼びません。ただし、許可とも呼びません。回答は not_checked であり、実際に起きたことを示します。ルールがあるかどうかにかかわらず、こちらでは確認できなかったということです。
したがって、ゲートを設けるのはあなたです。 利用にあたって robots の遵守が必要なら、このフィールドに基づいて分岐してください。robots == "disallowed_by_site" は、サイトが禁止していることを意味します。遵守する場合は、not_checked も停止の条件として扱ってください。これはルールを読み取れなかったという意味であり、ルールが存在しないという意味ではありません。
📖 動作の仕組み
- ALGORITHM.md — 呼び出しごとに何が起きるのかを、手順ごとに説明します。
- contracts/ — 呼び出しの契約です。実装コードとは別にバージョン管理されています。
- measures/ — 日付付きの測定結果です。どのエンジンが稼働していたか、負荷の段階ごとに何が得られるか、トランスポートの変更で何が改善されたかを記録しています。測定内容と測定時期を添えた数値です。
- contracts/ag.search.v3.md — 関連する各ファイルとともに、それぞれの機能が保証する内容を説明します。また、保存済みの回答を読み取る必要がある方向けに、
/1および/2からの変更点をまとめた表もあります。
🔒 デプロイと公開範囲
wiring/expose-localhost.yml はアダプターを 127.0.0.1 のみに公開します。提供されるオーバーレイはこれだけです。それより広い範囲に公開する場合は、独自のオーバーレイを作成してください。wiring/expose-localhost.yml のヘッダーには、最初に確認すべき点が記載されています。Docker は公開ポートへのトラフィックを DNAT 後に FORWARD 経由で転送しますが、ファイアウォール独自のチェーンはそのフックより前にあります。そのため、「閉じている」と表示するファイアウォールでも、公開ポートがインターネットに開放されている可能性があります。
外部に公開された検索サーバーは、マシンの所有者の名前でネットワークにアクセスするオープンプロキシです。
このリリースで確認されている制限事項を、いずれも未記載と誤解されないよう明記します。
- ブラウザー経由のページは、ネットワークではなく、ブラウザー内のスクリプトとルーティングによって境界の内側に保たれます。 未解決のケースは3つあり、それぞれ内容が異なります。DNS リバインディングは防止されず、ページも保留されません。内外の判定はセッション中保持され、ブラウザーは自身の名前解決結果が返す宛先に接続します。内側に向かうリダイレクトのホップと、ページのスクリプトから到達できない投機的ルールによるプリフェッチ(閉じた宣言的シャドウルート内にある場合、またはホストで一時停止したパーサーが作成した開いたシャドウルート内にある場合)は検出され、ページは保留されますが、いずれもリクエストが行われた後です。ページが開いたポップアップによるプリフェッチは監視されず、そのページも保留されません。0.3.5 以降、ページのスクリプトから到達できるルールは削除されています。全リストは contracts/ag.read.v2.md にあります。この制限を解消するには、サイドカー側にプロキシまたは送信トラフィックのルールが必要です。
- ドアログに誤った転送元 IP アドレスが記録されることがあります。 キープアライブ接続では、ヘッダーの解析前に拒否されたリクエスト(400、414、431、505)が、直前のリクエストの
X-Forwarded-Forを使って記録されます。このフィールドはどちらの場合も未検証と示されます。これは0.3.5で未解決であり、次のリリースで修正されます。 - ディープサーチは、自身のキューによる拒否をプロバイダーによるものとして記述します。 モデルスロットが時間内に空かない場合、ドキュメントに記載されているとおり、回答の
causeはbusyになります。しかし、errorのテキストはLLM_MAX_CONCURRENTを示さず、モデルがレート制限されていると表示します。誤っているのはテキストだけです。 BROWSER_WS_SECRETはサイドカー内の root から参照できます。 tini とサイドカーのスーパーバイザーは、起動時の環境にこの値を保持しており、コンテナー内の root は/proc経由で読み取れます。サイドカーは root として実行され、ブラウザーは--no-sandboxで実行されるため、レンダラーを乗っ取ったページがこの値を読み取れる可能性があります。その時点で、ページはすでに、そのシークレットが保護するサイドカー内で root 権限を取得しています。web_readの呼び出し上限は、その下位のすべての処理を制限するものではありません。 ハングしたブラウザーセッションや、停止処理の直前に始まったスキャンページのレンダリングは、上限を超えて終了する場合があります。0.3.5 以降は、空きモデルスロットを待つ時間とモデルの回答全体が、この上限の対象です。HOWTO-CALL.md の制限事項の表を参照してください。
✅ テスト
IMAGE=ag-mod-search/adapter:0.3.5 bash tests/in-image.sh
4つのテストスイートで構成されています。偽のメタサーチを使ったプロトコルと検索のテスト、偽サイトを使った読み取りのテスト、メモリー上に構築したデータベースを使った計算済みプールのテスト、定型の応答を使ったモデルプロバイダーの回答解析のテストです。いずれも外部へのリクエストは一切行いません。 また、ランナーは信頼に頼らず、--network none でそれを保証します。検索よりも読み取りで重要です。インターネットにアクセスするテストは、ツールが保護するまさにそのリソース、つまりツールが呼び出し元として使う唯一のアドレスの評判を損なう可能性があるためです。
テストは、コードを編集するマシンではなく、ビルド済みイメージ内で実行します。PDF パーサーはイメージ内にあり、イメージの外でテストスイートを実行すると、それを使うすべてのテストがスキップされるためです。スキップは通知なしには行われません。チェックは赤になり、どこで実行すべきかを示すメモが表示されます。
これらのテストスイートで確認できない内容は、tests/README.md に記載されています。
🤝 貢献
機能、エンジン、ヒューリスティックは、参照属性が宣言され、確認済みの質問プールが付属するまでは受け入れられません。参照属性とは、テスト対象自身からは分からない正解の特性です。CONTRIBUTINGをご覧ください。
📄 ライセンス
GNU Affero General Public License v3.0。実行、変更、拡張は自由です。変更を加えたものをネットワーク経由で他者に提供する場合、変更内容も同じライセンスで公開する必要があります。これが唯一の義務であり、サービスの運用も提供にあたります。
この条項を受け入れられない方は、フォークを作るのではなく、商用ライセンスについてお問い合わせください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。