0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zoom の AI Companion に custom agent としてMCP サーバ追加する方法

0
Last updated at Posted at 2026-01-07

はじめに

Gemini_Generated_Image_tckvpmtckvpmtckv.png

ZoomのAI Companion が MCP(Model Context Protocol)に対応 したので、自作のテスト MCP サーバーを接続してWeb検索機能を追加してみました。AI Companionには もともとWeb検索自体あるのですが、MCPのプロトコルに対応していれば機能連携できる! ということの確認のため、この記事ではその簡単なテストとして SearXNG(オープンソースのメタ検索エンジン)とFastMCPを使って、Zoom AI Companionに検索機能を追加する手順を解説します。

完成イメージ

スクリーンショット 2026-01-07 15.39.31.png

今回のテストでは、AI Companionに「〇〇についての論文をリストアップして」と聞くと、リンクと共にそれぞれの論文の要約を回答してくれるものを作りました。

アーキテクチャ

┌─────────────────────┐
│  Zoom AI Companion  │
└──────────┬──────────┘
           │ HTTPS (ngrok経由)
           ▼
┌─────────────────────┐
│   FastMCP Server    │  ← Python
│   (port 8000)       │
└──────────┬──────────┘
           │ localhost
           ▼
┌─────────────────────┐
│    SearXNG          │  ← Docker
│   (port 8888)       │
└─────────────────────┘

そもそも Zoom AI Companion とは

念のため、簡単に Zoom AI Companion とは何か、を解説しておきます!

Zoom AI Companionは、Zoomに統合されたAIアシスタント機能です。会議の生産性を向上させ、日々のコミュニケーションをよりスムーズにするために設計されています。

主な機能

  • 会議の要約: 会議中の重要なポイントを自動的に抽出し、要約を作成します。会議後すぐに内容を振り返ることができます
  • メッセージの作成支援: メッセージの下書きや、文章の改善提案を受けることができます
  • アクションアイテムの抽出: 会議から次のステップやタスクを自動的に識別し、リスト化や Zoom タスクへの追加を行います
  • ドキュメントの生成: 関連する資料や議事録、その他連携しているサービスの資料を参照し、ドキュメントを作成します
  • 質問への回答: 会議の内容について質問すると、録画や記録から関連情報を見つけて回答してくれます

詳細は、ぜひこちらの **Zoom AI Zoom AI Companion 3.0のページをご確認ください。日々アップデートされ、どんどん使いやすくなっています。

スクリーンショット 2026-01-07 16.30.17.png

前提条件

今回ご紹介する MCP サーバ連携について、以下の準備が必要となります。

  • Zoomアカウント: Business以上 + Custom AI Companion アドオン
    (Revenue Acceleratorなどのライセンスがある場合、Custom AI Companionが含まれているため、追加のアドオンは不要です)
  • Python: 3.10以上
  • Docker: SearXNG用
  • ngrok: ローカルサーバー公開用
  • uv: Pythonパッケージマネージャー(推奨)

Step 1: SearXNGをDockerで起動

SearXNGは70以上の検索エンジンの結果を集約する無料のメタ検索エンジンです。この記事はSearXNGの紹介が目的ではないので、この部分については他の記事等で調べていただければと思います。以下、簡単にDockerで立ち上げておきます。

# SearXNG起動
docker run -d \
  --name searxng \
  -p 8888:8080 \
  -e "BASE_URL=http://localhost:8888/" \
  -e "INSTANCE_NAME=my-searxng" \
  searxng/searxng

ブラウザで http://localhost:8888 にアクセスして、検索画面が表示されればOKです。

JSON APIを有効化

デフォルトではJSON出力が無効なので、コンテナに入って有効化します。

# コンテナに入る
docker exec -it searxng sh

# 設定ファイルを編集
vi /etc/searxng/settings.yml

search: セクションに以下を追加:

search:
  formats:
    - html
    - json  # これを追加

コンテナを再起動:

docker restart searxng

動作確認:

curl "http://localhost:8888/search?q=test&format=json" | head

JSONが返ってくれば成功です。

Step 2: MCPサーバーを作成

プロジェクトセットアップ

# ディレクトリ作成
mkdir web-search-mcp && cd web-search-mcp

# uvで初期化
uv init

pyproject.toml

[project]
name = "web-search-mcp"
version = "1.0.0"
description = "Web Search MCP Server for Zoom AI Studio"
requires-python = ">=3.10"
dependencies = [
    "mcp>=1.0.0",
    "httpx>=0.27.0",
]

main.py

PythonのFastMCPを使います。また、今回はスクレイピングツールなどは使わずに直接取得するため、httpxもインポートしています。

"""
Web Search MCP Server for AI Studio
Using FastMCP + SearXNG
"""

from mcp.server.fastmcp import FastMCP
import httpx
from typing import Optional

# SearXNG endpoint
SEARXNG_URL = "http://localhost:8888"

mcp = FastMCP("web-search-server")

最初に検索ツールとして web_search を定義します。

@mcp.tool()
async def web_search(
    query: str,
    categories: Optional[str] = None,
    language: Optional[str] = "auto",
    max_results: Optional[int] = 10
) -> dict:
    """
    Search the web using SearXNG metasearch engine.
    
    Args:
        query: Search query string
        categories: Comma-separated categories (general, images, news, videos, etc.)
        language: Language code (en, ja, auto, etc.)
        max_results: Maximum number of results to return (default: 10)
    
    Returns:
        Search results with titles, URLs, and snippets
    """
    params = {
        "q": query,
        "format": "json"
    }
    
    if categories:
        params["categories"] = categories
    
    if language and language != "auto":
        params["language"] = language
    
    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.get(f"{SEARXNG_URL}/search", params=params)
            response.raise_for_status()
            data = response.json()
        
        results = []
        for item in data.get("results", [])[:max_results]:
            results.append({
                "title": item.get("title", "No title"),
                "url": item.get("url", ""),
                "snippet": item.get("content", ""),
                "engines": item.get("engines", [])
            })
        
        return {
            "query": query,
            "total": len(results),
            "results": results
        }
    
    except httpx.ConnectError:
        return {
            "error": f"Cannot connect to SearXNG at {SEARXNG_URL}",
            "query": query,
            "total": 0,
            "results": []
        }
    except Exception as e:
        return {
            "error": str(e),
            "query": query,
            "total": 0,
            "results": []
        }

さらにURLからテキスト抽出するhttpxを利用するツール web_fetch を追加します。

@mcp.tool()
async def web_fetch(
    url: str,
    max_length: Optional[int] = 50000
) -> dict:
    """
    Fetch a web page and return its text content.
    
    Args:
        url: URL of the page to fetch
        max_length: Maximum character length of output (default: 50000)
    
    Returns:
        Page title and text content
    """
    try:
        async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
            response = await client.get(
                url,
                headers={
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
                }
            )
            response.raise_for_status()
            html = response.text
        
        import re
        
        # Remove script and style
        html = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL | re.IGNORECASE)
        html = re.sub(r'<style[^>]*>.*?</style>', '', html, flags=re.DOTALL | re.IGNORECASE)
        
        # Extract title
        title_match = re.search(r'<title[^>]*>(.*?)</title>', html, re.IGNORECASE | re.DOTALL)
        title = title_match.group(1).strip() if title_match else "No title"
        
        # Remove HTML tags
        text = re.sub(r'<[^>]+>', ' ', html)
        text = re.sub(r'\s+', ' ', text).strip()
        
        # Truncate if needed
        truncated = len(text) > max_length
        if truncated:
            text = text[:max_length] + "... [truncated]"
        
        return {
            "url": url,
            "title": title,
            "content": text,
            "length": len(text),
            "truncated": truncated
        }
    
    except Exception as e:
        return {
            "error": str(e),
            "url": url,
            "title": "",
            "content": "",
            "length": 0,
            "truncated": False
        }


# Server settings
mcp.settings.host = "0.0.0.0"
mcp.settings.port = 8000


if __name__ == "__main__":
    mcp.run(transport="streamable-http")

依存関係インストール

uv sync

Step 3: MCPサーバーを起動

uv run main.py

以下のようなログが表示されれば成功:

INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     StreamableHTTP session manager started
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Step 4: ngrokで公開

別のターミナルで:

ngrok http 8000 --host-header=rewrite

ngrok無料版の場合 --host-header=rewrite オプションが必須です。これがないとZoom側で接続エラーになります。

ちなみに、以前の記事でもお伝えしましたが ngrok の無料プランでも、アカウントを作成すると固定 URL(Static Domain)が1つ払い出されます。これにより、毎回URLが変わる問題を回避できます。これを使う場合は、

# 固定ドメインで起動(アカウント作成後に利用可能)
ngrok http 8000 --domain=your-subdomain.ngrok-free.app --host-header=rewrite

固定ドメインは ngrok Dashboard から確認・設定できます。

表示されるHTTPS URL(例: https://xxxx.ngrok-free.app)をメモします。

Step 5: AI Studioからに接続

  1. Zoom Web Portal にログイン
  2. 左メニュー → AI StudioTool TemplatesMCP タブを選択
  3. Connect ボタンをクリック
  4. 以下を入力:
    • Name: Web Search MCP
    • Description: Web search using SearXNG
    • MCP Server URL: https://xxxx.ngrok-free.app/mcp
  5. Connect をクリック

接続成功すると、ツール一覧に web_searchweb_fetch が表示されます。

Step 6: Custom Agentを作成

  1. AI StudioTool templatesCustom Agents
  2. + Custom Agent をクリック
  3. 適当な名前を付けて作成(例:「Web Search Agent」)
  4. Tools セクションで、作成したMCPツールを追加
  5. Publish で公開

動作確認

Zoom WorkplaceのAI Companionパネルで、 + ボタンをクリックした上で作成したエージェントを選択し、質問してみましょう:

  • 「三体問題に関する最新の研究をまとめて」
  • 「光の光の粒子・波動性の相補性に関する最新の論文を調べて」

MCPサーバ側にリクエストが届き、ログが出てくると思います。

2.gif

トラブルシューティング

接続エラーになる場合

  1. ngrokのオプション確認: --host-header=rewrite を付けているか
  2. SearXNGの起動確認: curl http://localhost:8888 でアクセスできるか
  3. MCPサーバーのログ確認: エラーが出ていないか

ツールが表示されない場合

Zoom AI Studio側で一度削除して、再度Connectしてみてください。

検索結果が返ってこない場合

SearXNGのJSON APIが有効になっているか確認:

curl "http://localhost:8888/search?q=test&format=json"

技術的なポイント

なぜFastMCPを使うのか

Zoom Custom AI Companion の Custom Agent は、MCPサーバに以下を期待しています:

  • Streamable HTTP transport: SSE対応のHTTPエンドポイント
  • セッション管理: リクエストごとのセッションID管理
  • MCP Protocol Version 2025-03-26: 現状採用されているMCPバージョン

FastMCPはこれらを内部で自動的に処理してくれるため、ツールの実装に集中できます。

発展的な使い方

Playwrightでフルスクレイピング

現在の web_fetch は静的HTMLの取得のみですが、JavaScriptレンダリングが必要な場合は Playwright を追加できます:

from playwright.async_api import async_playwright

@mcp.tool()
async def web_scrape_full(url: str) -> dict:
    """Full page scraping with JavaScript rendering"""
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto(url)
        content = await page.content()
        await browser.close()
    # ... 以下、HTML処理

特定ドメイン専用の検索

動画検索に特化させる場合:

@mcp.tool()
async def search_videos(query: str) -> dict:
    """Search videos only"""
    return await web_search(
        query=query,
        categories="videos", #カテゴリをvideosに固定
        max_results=20
    )

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?