Skip to main content
Glama

research-mcp

ステートレスなMCPファサードで、検索/読み取りプロバイダのピラミッドを単一のstreamable-http MCPエンドポイントの背後に隠し、優れたロシア語のヘルプテキストを持つ3つのクリーンなツールだけを公開します。LLMはシンプルな「検索→読み取り」ツールセットを取得します。その背後では、複数のプロバイダが自動的に試行され、マージされ、フェイルオーバーされます。

このアプリは認証を行いません — ホスト上でTraefik + basicAuthを介して公開されます。アプリケーションの状態は保持されません。永続化されるのはdata/ディレクトリ(ボリューム上に保持)のログファイルのみです。

Tools

Tool

説明

web_search(query, num_results=8, page=1, language=None)

有効なすべてのプロバイダを横断して検索し、マージ+重複排除→ランク付けされたリスト(タイトル、URL、スニペット)を返します。検索のみ。

read_page(url)

1つのページまたはPDF→クリーンなMarkdown。タイプを自動検出し、読み取りパイプライン(軽量→重量)を成功するまで順に試行します。

read_pages(urls)

最大20のURLを並行して処理→{url, ok, markdown|error}のリスト。

Related MCP server: Web Search MCP

Architecture: types + instances

プロバイダはプラグインです。以下を分離します:

  • タイプ — 実装クラス(例:searxng検索プロバイダ)。src/providers/内の各モジュールに1つ存在し、@register("type")で登録されます。

  • インスタンス — タイプの設定済みコピーで、シークレット/URLが名前付き環境変数から解決されます(1つのタイプの複数インスタンスが許可されます。例:異なるキーを持つtavily-1 / tavily-2)。

どのインスタンスが存在し、各パイプラインがそれらを試行する順序はコード内(src/pipeline_config.py)で設定されます。キー/URLは環境変数名によってENVから取得されます。

  • 検索パイプライン(searxng → brave → jina-search → serper → exa):有効なインスタンスは並行して実行されます。結果は正規化されたURLでマージされ、重複排除されます(パイプラインの前の位置が優先)。JINA_API_KEYが設定されている場合(かつSEARCH_RERANK_ENABLEDがオフでない場合)、マージされた全リストはjina-reranker-v3.5によって再ランク付けされ、num_resultsへの切り詰めが盲目的なパイプライン順序のプレフィックスではなく、最も関連性の高いヒットを保持します。再ランク付けが失敗した場合は、マージ順序にフォールバックします。 searxngとbraveはさらにローカルでスロットルをかけます(それぞれ45秒に1クエリ、1.1秒に1クエリ。測定された上流の制限に一致)。スロットが占有されている場合、待機する代わりに現在の検索をスキップします。

  • 読み取りパイプライン(trafilatura → jina → crawl4ai → tavily-1 → tavily-2 → firecrawl):単一のプローブGETがURLを分類します。PDF(Content-Type / .pdf / %PDFマジック)はpypdfで抽出されます。HTMLの場合、その同じボディがtrafilaturaに渡されるため、ホットパスは2回GETしません。その後、残りのインスタンスが順に試行され、>= FALLBACK_MIN_CHARSのコンテンツを返す最初のインスタンスが勝ちます。

横断的:短いバックオフ付きの一時的なリトライを1回(5xx / トランスポートエラー)。402(クレジット不足)/ 429(レート制限)はプロバイダの失敗として扱われ、次のインスタンスに進みます(これがtavily-1 → tavily-2のフェイルオーバーを実現します)。

インスタンスは、必要な環境変数が設定されている場合にのみ有効になります。それ以外の場合はログ行とともにスキップされます。trafilaturaは設定不要(常にオン)です。jinaはキーレスで動作します(キーはオプション)。起動時にサーバーは少なくとも1つの検索インスタンスと1つの読み取りインスタンスを必要とし、ない場合は明確なメッセージで終了します。

Adding a provider

  1. src/providers/<type>.pyを作成し、@register("<type>")でデコレートされたクラスがSearchProvider.search(...)またはReadProvider.read(...)を実装します。

  2. src/providers/__init__.pyでモジュールをインポートします(デコレータが実行されるように)。

  3. src/pipeline_config.pyにInstance("name", "<type>", api_key_env="YOUR_ENV_NAME")の行を追加し、SEARCH_PIPELINE / READ_PIPELINEでそのnameを参照します。環境変数名を使用し、値は使用しないでください。

  4. .env.exampleに環境変数を文書化します。

Quick start

make install                # create .venv + install dev/test deps
cp .env.example .env        # fill in the keys you have  (shortcut: make env)
make test                   # run tests
make run                    # run the server (streamable-http on MCP_HOST:MCP_PORT, endpoint /mcp)

Configuration

すべての設定はENV / .envから取得されます(.env.exampleを参照)。プロバイダのシークレット/URLはインスタンスローダーで名前によって読み取られ、Settingsフィールドとして宣言されません。非シークレットのノブ(すべてデフォルトあり):MCP_HOST、MCP_PORT、LOG_LEVEL、LOG_FILE、LOG_ROTATION、LOG_RETENTION、REQUEST_TIMEOUT、FALLBACK_MIN_CHARS、READ_PAGES_CONCURRENCY、RETRIES、SEARCH_RERANK_ENABLED、JINA_TOKEN_BUDGET。read_pagesの呼び出しごとのURL上限は固定の20(ハード定数、ツールの説明と一致)であり、設定不可です。

プロバイダ環境変数:SEARXNG_URL、BRAVE_API_KEY、SERPER_API_KEY、EXA_API_KEY、JINA_API_KEY(1つのキーでjinaリーダーがキー付きモードで有効になり、jina-searchプロバイダと検索リランカーも有効になります。リーダー単体はキーレスでも動作します)、CRAWL4AI_URL + CRAWL4AI_TOKEN、TAVILY_1_API_KEY、TAVILY_2_API_KEY、FIRECRAWL_API_KEY。

Proxy

外部インスタンスは、<INSTANCE>_PROXYを設定することで、独自のSOCKS5/HTTPプロキシを経由させることができます。これは、IPベースのブロック(例:Exaの前のCloudflare)を回避するためのクリーンな出口に役立ちます。インスタンスごとにサポート:EXA_PROXY、BRAVE_PROXY、SERPER_PROXY、JINA_PROXY、TAVILY_1_PROXY、TAVILY_2_PROXY、FIRECRAWL_PROXY。内部インスタンス(searxng、crawl4ai、trafilatura)にはプロキシがありません。

値はそのままhttpxに渡されます。socks5://host:portはプロキシ側DNSを実行します(ターゲットのホスト名はプロキシによって解決されます。curl --socks5-hostnameと同様)。socks5h:// / http://host:portも受け入れられます。未設定の場合、そのインスタンスは直接接続します。パイプラインは、プロキシURLごとに1つのプールされたhttpxクライアント(および1つの直接クライアント)を保持し、インスタンスごとに選択されるため、プロキシ経由と直接のプロバイダが並行して実行されます。socksエクストラ(httpx[socks]、すでに固定)が必要です。

Logging

stderr(Dockerのローテーション上限付きjson-fileドライバーでキャプチャ)に加えて、サーバーは永続的なログファイルをdata/research-mcp.log(デフォルト。LOG_ROTATION=20 MB、LOG_RETENTION=14 days)に書き込みます。これはdata/ボリューム上にあるため、コンテナの再起動やイメージの更新後も存続します。ファイルには、ツール呼び出しごとにリクエストごとの行が1つ含まれます — 検索(query、実際に実行されたプロバイダインスタンス、結果数、レイテンシ)と読み取り(url、勝利したプロバイダ/ティアまたはpdf、ok、レイテンシ)、さらにread_pages count=N ok=Kの要約 — これにより、リクエストがプロバイダティア全体にどのように分散するかを分析するのに役立ちます。リクエストボディやシークレットはログに記録されません。URL/クエリ、プロバイダ名、カウント、タイミングのみです。

Deployment

Gitea Actionsがイメージをビルドし、Giteaレジストリgitea.vvzvlad.xyz/projects/research-mcpにプッシュします(test → build、タグlatest + sha)。本番ではdocker-compose.ymlを介してビルド済みイメージをプルします(Traefik + basicAuthの背後、watchtowerがlatestを自動更新。data/ボリュームが更新間でログファイルを保持)— 本番でビルドすることはありません。

Layout

Path

目的

src/providers/base.py

プロバイダインターフェース + SearchResult / ProviderError。

src/providers/registry.py

@registerデコレータ → REGISTRY。

src/providers/<type>.py

プロバイダタイプごとに1つのモジュール。

src/providers/pdf.py

PDF検出 + pypdfテキスト抽出(パイプラインで使用)。

src/pipeline_config.py

コード内のインスタンス + パイプライン順序。

src/pipeline.py

インスタンスローダー + 検索/読み取りロジック。

src/rerank.py

JinaReranker — 検索結果のマージ後リランク。

src/settings.py

非シークレットのノブ(pydantic-settings)。

src/server.py

3つの@mcp.tool定義を持つbuild_server()。

main.py

薄いエントリポイント:サーバーをビルドし、streamable-httpを実行。

tests/

pytestスイート(ネットワークはrespxでモック)。

Maintenance

ActivityActive
ResponsivenessUnresponsive

Related MCP Connectors

Related MCP Servers