VertexAI Searchを使うために(2)

昔取った杵柄

まさかMCPサーバを作ることになるとは・・

認証トークンの寿命は1時間しかない

VertexAI SearchのAPIについて、作った後のApplication Propertyを参照すると、以下のような内容になっている。ぶっちゃけいうと、以下文章にある $(gcloud auth print-access-token) に記述されているベアラートークンをGoogle Cloud Console使って出力させたらREST APIで呼び出せるようになる。

curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
"https://discoveryengine.googleapis.com/v1alpha/projects/<project-id>/locations/global/collections/default_collection/engines/<search-engine-id>/servingConfigs/default_search:search" \
-d '{"query":"<QUERY>","pageSize":10,"queryExpansionSpec":{"condition":"AUTO"},"spellCorrectionSpec":{"mode":"AUTO"},"languageCode":"ja","contentSearchSpec":{"extractiveContentSpec":{"maxExtractiveAnswerCount":1}},"userInfo":{"timeZone":"Asia/Tokyo"}}'

が・・・・現実は甘くない。このトークンの有効時間が1時間しかないからだ。さて、どーしたらいいのか?という話なんだが、こうなってしまうとAPIアクセス・認証部分は作りこみをするしかないと私は結論付けた。

VertexAI向けMCPサーバを作ってみる

MCPサーバを作るにあたりベースとなるものが欲しかった。今回、手あたり次第に探し回った結果、ここがよかろうというポイントとして

https://github.com/ShintaroMorimoto/vais-mcp

のリポジトリで示されるプロダクトを拝借させてもらった。この中の以下のファイルを書き換えていく。
上記のリポジトリ様で作られたコードは、いわゆるClaude Codeなどのような開発ツールに直付けして利用する形態をとっており、Difyから触るにはこれではNG。必ずSSEにする必要がある。

※実はStreamableHTTPでもいいのだが、今なかなかどうしてうまくプロセス管理ができてないので、SSEで行く。

src/vais_mcp/server.py

これは全面的に書き換える。

import sys
import asyncio

from fastmcp import FastMCP
from loguru import logger

from .config import get_settings
from .vais import VaisError, call_vais

settings = get_settings()

logger.remove()
logger.add(sys.stderr, level=settings.LOG_LEVEL)


mcp = FastMCP(
    name="Vertex AI Search MCP",
    description="Vertex AI Search MCP server",
    log_level=settings.LOG_LEVEL,
)


@mcp.tool()
async def search_vais(
    search_query: str,
) -> dict:
    logger.info(f"Received search request with query: '{search_query}'")
    if not search_query:
        logger.warning("Search query is empty.")
        return {"response": "No search query provided"}

    try:
        # 複数ユーザの同時リクエスト時にイベントループがブロックされないように、
        # 同期関数である call_vais を別スレッドにオフロードして実行します。
        response_data = await asyncio.to_thread(
            call_vais,
            search_query=search_query,
            google_cloud_project_id=settings.GOOGLE_CLOUD_PROJECT_ID,
            impersonate_service_account=settings.IMPERSONATE_SERVICE_ACCOUNT,
            vais_engine_id=settings.VAIS_ENGINE_ID,
            vais_location=settings.VAIS_LOCATION,
            page_size=settings.PAGE_SIZE,
            max_extractive_segment_count=settings.MAX_EXTRACTIVE_SEGMENT_COUNT,
        )
        logger.info(f"Search request successful, returning {len(response_data)} items.")
        return {"response": response_data}
    except VaisError as e:
        logger.error(f"Error processing search request: {e}")
        return {"error": str(e), "status_code": 500}


def main():
    logger.info("Starting FastMCP server.")
    if settings.MCP_TRANSPORT == "sse":
        logger.info(f"Starting in SSE mode on {settings.MCP_HOST}:{settings.MCP_PORT}")
        mcp.run(transport="sse", host=settings.MCP_HOST, port=settings.MCP_PORT)
    else:
        logger.info("Starting in stdio mode")
        mcp.run()


if __name__ == "__main__":
    main()

src/vais_mcp/config.py

一部追加します。

from functools import lru_cache
from typing import Optional

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    GOOGLE_CLOUD_PROJECT_ID: str
    IMPERSONATE_SERVICE_ACCOUNT: Optional[str] = None
    USE_MOUNTED_SA_KEY: bool = False
    CONTAINER_SA_KEY_PATH: str = "/app/secrets/sa-key.json"

    VAIS_ENGINE_ID: str
    VAIS_LOCATION: str = "global"
    PAGE_SIZE: int = 5
    MAX_EXTRACTIVE_SEGMENT_COUNT: int = 2

    LOG_LEVEL: str = "WARNING"

    # --- 追加: SSE / サーバー設定 ---
    MCP_TRANSPORT: str = "stdio"
    MCP_HOST: str = "0.0.0.0"
    MCP_PORT: int = 8000

    model_config = SettingsConfigDict(extra="ignore", env_ignore_empty=True)


@lru_cache
def get_settings() -> Settings:
    return Settings()

記述の中でなぜかMCP_TRANSPORTがstdioになってますが、そこは無視してよいです。後できちんとsse仕様にします。

Dockerfile

SSE仕様に変更していくため、以下の内容にしていきます。
私の環境ではPodmanを使って疑似的にDockerっぽくして動かしていることもあり、BuildKitがありません。そこで、BuildKit向けに作られてるところを順次通常のDockerfile構文に書き換えていきます。

SSEモードを使用する宣言を記述しているのもこちらです。ENVで環境変数の設定を行っています。

FROM python:3.13-slim AS builder

WORKDIR /app

COPY requirements.setup.txt .
RUN python -m pip install --no-cache-dir -r requirements.setup.txt

ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
ENV UV_PYTHON_PREFERENCE=only-system
ENV UV_FROZEN=true

COPY pyproject.toml uv.lock ./

# BuildKitのキャッシュマウント(--mount=type=cache)を削除し、通常のRUNに変更
RUN uv sync --frozen --no-install-project --no-dev --no-editable

COPY . /app

# こちらも同様にキャッシュマウントを削除
RUN uv sync --frozen --no-dev --no-editable

FROM python:3.13-slim

WORKDIR /app

RUN groupadd --system app && useradd --system --gid app app
RUN mkdir -p /app && chown -R app:app /app
RUN mkdir -p /app/secrets && chown -R app:app /app/secrets

COPY --from=builder /usr/local/bin/uv /usr/local/bin/uv
COPY --from=builder --chown=app:app /app/.venv /app/.venv
COPY --from=builder --chown=app:app /app /app

ENV PATH="/app/.venv/bin:$PATH"

ENV UV_CACHE_DIR=/app/.uv_cache
RUN mkdir -p /app/.uv_cache && chown -R app:app /app/.uv_cache

# --- SSE サーバーの設定 ---
ENV MCP_TRANSPORT=sse
ENV MCP_HOST=0.0.0.0
ENV MCP_PORT=8000
EXPOSE 8000
# -------------------------

USER app

ENTRYPOINT ["uv", "run", "vais-mcp"]
CMD

Dockerコンテナの作成

やっとコンテナとして形成できるようになったので、まずイメージを作る。

$ docker build -t vais-mcp:0.01 .

イメージを一覧表示してみて、できたことを確認。

$ docker image ls
REPOSITORY                      TAG           IMAGE ID       CREATED         SIZE
vais-mcp                        0.01          903509fbf393   3 hours ago     366MB
<none>                          <none>        22cfee17f015   3 hours ago     340MB
<none>                          <none>        d668eae0f186   3 hours ago     236MB
<none>                          <none>        1c55b3b58ec8   4 hours ago     236MB
<none>                          <none>        dfcb507b43f0   4 hours ago     236MB
vais-mcp-vais-mcp               latest        2b27066ead79   4 hours ago     509MB
langgenius/dify-api             1.13.3        9b188598cfa1   6 days ago      2.92GB
langgenius/dify-web             1.13.3        17b217cdd2c5   6 days ago      341MB
langgenius/dify-sandbox         0.2.14        469ccccb7ceb   6 days ago      570MB
nginx                           latest        0cf1d6af5ca7   6 days ago      161MB
python                          3.13-slim     9aafbb8a9ec1   2 weeks ago     118MB
postgres                        15-alpine     cd848ee12e8e   4 weeks ago     274MB
redis                           6-alpine      d81ff0fbad3b   2 months ago    30.2MB
langgenius/dify-plugin-daemon   0.5.3-local   c138e8c0e115   2 months ago    1.49GB
ubuntu/squid                    latest        f49c57d20819   4 months ago    205MB
supercorp/supergateway          latest        f1a9af3f2ce8   5 months ago    171MB
mrmtsntr/vais-mcp               latest        a18ad8b48976   10 months ago   353MB
semitechnologies/weaviate       1.27.0        f24b5f0e68e6   17 months ago   161MB
busybox                         latest        925ff61909ae   18 months ago   4.42MB

一番上にイメージができたことを確認した。

docker-compose.yaml

今回、私はdocker-compose-v2を使って動かしたかったので、docker-compose.yamlファイルを作成する。

services:
  vais-mcp:
    image: vais-mcp:0.01
    ports:
      - "8000:8000"
    restart: always
    environment:
      MCP_TRANSPORT: sse
      MCP_HOST: 0.0.0.0
      MCP_PORT: 8000
      LOG_LEVEL: INFO
      USE_MOUNTED_SA_KEY: true
      CONTAINER_SA_KEY_PATH: /app/secrets/sa-key.json
      GOOGLE_CLOUD_PROJECT_ID: <your-project-id>
      VAIS_ENGINE_ID: <search-engine-id>
      VAIS_LOCATION: global
      PAGE_SIZE: "10"
      MAX_EXTRACTIVE_SEGMENT_COUNT: "5"
      IMPERSONATE_SERVICE_ACCOUNT: "vertex-express@<project-id>.iam.gserviceaccount.com"

これで docker compose up -d をすれば実行を開始する。

Difyに登録

次はDifyに登録する。
Difyに登録する場合、以下のように http://<MCPサーバのIPアドレス>:8000/sse を指定することで接続可能になる。認証等は特に仕組みとして組み込んでないので、これとサーバ識別子を追加すれば、「保存」を押すだけで登録されることになる。

無事に以下のようになったら登録完了である。

なお、起動開始してからその後登録までの間のログは以下のような感じで出力されている。

vais-mcp-1  |    Building vais-mcp @ file:///app
vais-mcp-1  |       Built vais-mcp @ file:///app
vais-mcp-1  | Uninstalled 2 packages in 13ms
vais-mcp-1  | Installed 4 packages in 6ms
vais-mcp-1  | 2026-03-31 13:07:42.239 | INFO     | vais_mcp.server:main:53 - Starting FastMCP server.
vais-mcp-1  | 2026-03-31 13:07:42.239 | INFO     | vais_mcp.server:main:55 - Starting in SSE mode on 0.0.0.0:8000
vais-mcp-1  | [03/31/26 13:07:42] INFO     Starting server "Vertex AI Search     server.py:207
vais-mcp-1  |                              MCP"...
vais-mcp-1  | INFO:     Started server process [37]
vais-mcp-1  | INFO:     Waiting for application startup.
vais-mcp-1  | INFO:     Application startup complete.
vais-mcp-1  | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
vais-mcp-1  | INFO:     172.20.0.1:34926 - "GET /sse HTTP/1.1" 200 OK
vais-mcp-1  | INFO:     172.20.0.1:34942 - "POST /messages/?session_id=4044bc5e8e4e46938963ce14132a409c HTTP/1.1" 202 Accepted
vais-mcp-1  | INFO:     172.20.0.1:34948 - "POST /messages/?session_id=4044bc5e8e4e46938963ce14132a409c HTTP/1.1" 202 Accepted
vais-mcp-1  | INFO:     172.20.0.1:34962 - "POST /messages/?session_id=4044bc5e8e4e46938963ce14132a409c HTTP/1.1" 202 Accepted
vais-mcp-1  | INFO:     172.20.0.1:34962 - "GET /sse HTTP/1.1" 200 OK
vais-mcp-1  | INFO:     172.20.0.1:34970 - "POST /messages/?session_id=5057e0f95d9f44f793c984af4f4b2657 HTTP/1.1" 202 Accepted
vais-mcp-1  | INFO:     172.20.0.1:34978 - "POST /messages/?session_id=5057e0f95d9f44f793c984af4f4b2657 HTTP/1.1" 202 Accepted
vais-mcp-1  | INFO:     172.20.0.1:34978 - "GET /sse HTTP/1.1" 200 OK
vais-mcp-1  | INFO:     172.20.0.1:34982 - "POST /messages/?session_id=d53d518018384768bb43895e63d26511 HTTP/1.1" 202 Accepted
vais-mcp-1  | INFO:     172.20.0.1:34990 - "POST /messages/?session_id=d53d518018384768bb43895e63d26511 HTTP/1.1" 202 Accepted
vais-mcp-1  | INFO:     172.20.0.1:35006 - "POST /messages/?session_id=d53d518018384768bb43895e63d26511 HTTP/1.1" 202 Accepted
(.venv) aiuser@dproto01:~/vais-mcp$

さて問題は・・・

実は、まだ肝心なVertexAI Searchの検索エンジンというか、データストアが出来上がってない。

この画面下部にある「ステータス」がいつまでたっても「初期インデックスを登録中」から動かぬのだ。
検索を賭けても何も見つからないのはそういうことなんだろうなぁとは思うけど、これじゃぁあんまりだなぁと。プレビューデモ検索の仕様がないため、若干途方に暮れている。

も少し調べてみて、実際どのような検索が行えるのかは深堀確認していきたいところ。

一応お知らせ。
どうやらサイトの確認という行動を通じてこういうのは行えるらしい(クロールの手動実行とか)

味噌なのは、一度検索してデータストアにないことを確認後、そのURLをクリックしてチェックをONにするとボタンが「手動Crawl実行」と変化することだ。なるほど、そうか、そうして手動トリガーを送り込めるのか・・

コメント

タイトルとURLをコピーしました