【新人が Google Cloud を触ってみた】MCP + Cloud Run で 文書検索エージェント を作ってみた

こんにちは!SCSKの佐子です。

「MCP」という言葉を聞いたことがありますか?

MCPは、AIエージェントが外部データ/ツールを繋ぐための共通規格として注目されています。
ただ、実際にどう動いているかは、記事を読むだけではなかなかイメージが掴めませんでした。

そこで本記事では、「MCPサーバーをGoogle CloudのCloud Run上に構築し、AIエージェントから呼び出す」という一連の仕組みを検証してみます。

MCPとは?

大規模言語モデル(LLM)に、社内のデータやツールを操作できる「エージェント」として動いてもらうには、LLMと外部システムを繋ぐ共通の接続規格が必要になります。この規格の一つがMCP(Model Context Protocol)です。

従来の手法であるFunction Callingでは、モデルやプラットフォームごとに個別の実装が必要だったのに対し、MCPはコードの書き換えが不要です。そのため、MCPサーバーを1つ作っておけば、MCPに対応したあらゆるモデル(ClaudeやGemini)からそのまま利用可能となります。

Introducing the Model Context Protocol
The Model Context Protocol (MCP) is an open standard for connecting AI assistants to the systems where data lives, inclu...

MCPの構成

MCPには次の3つの役割があります。

MCP Host(ホスト)

ユーザーとやり取りするアプリケーションです。
今回の検証では「Geminiを使ったAgentプログラム」がホストに相当します。
ユーザーからの質問を受け取り、必要に応じて外部のツールを呼び出す判断をします。

MCP Client(クライアント)

Host内部に組み込まれ、MCPのプロトコルに従って実際に通信する部品です。
「ツールの一覧をください(tools/list)」「このツールをこの引数で実行してください(tools/call)」といったリクエストを、MCP Serverに送信します。

MCP Server(サーバー)

実際の検索やデータ取得などの「ツール」を提供する側です。
「どんなツールが使えるか」を外部に公開し、呼び出しがあれば実際の処理を行って結果を返します。

システム構成

システム構成図

 

  • Cloud Run①:Agent Webアプリ(Streamlit):ユーザーの質問を受け取り、Geminiに判断を委ねながら、必要に応じてMCP Clientを通じてツールを呼び出す
  • Vertex AI(Gemini):Agentが「どう答えるか、ツールを使うべきか」を問い合わせる先
  • Cloud Run②:MCP Server:モックデータへの検索・絞り込み機能をツールとして公開する
  • モックデータ:MCP Serverが検索対象とするJSONファイル

シーケンス図

フォルダ構成

MCP ServerとAgentは別々のCloud Runサービスとしてデプロイするため、別のフォルダに分けています。
gcloud run deploy --source .は、コマンドを実行したディレクトリの中身をまとめてビルド対象にするため、サービスごとにフォルダを分けておく必要があります

mcp-project/
├── mcp-server-demo/        ← Cloud Run①:MCP Server
│   ├── main.py              MCPサーバー本体(@mcp.tool()を定義)
│   ├── search_logic.py      検索・絞り込みのPythonロジック
│   ├── data.json            モックデータ(発信文書15件)
│   ├── requirements.txt     mcp[cli]<2
│   └── Dockerfile
│
└── mcp-pra-agent/           ← Cloud Run②:Agent Webアプリ
    ├── app.py                StreamlitのWeb UI+MCP Client
    ├── requirements.txt      google-genai / mcp[cli]<2 / streamlit
    └── Dockerfile

実装

モックデータの実装(data.json)

検索対象は、社内の発信文書を模したdata.jsonです(15件のうち1件を例示)。

コードを見る(クリックで開閉)
{
  "documents": [
    {
      "id": "DOC-003",
      "title": "社内システムメンテナンスのお知らせ(8月)",
      "category": "システムメンテナンス",
      "published_date": "2026-08-10",
      "tags": ["メンテナンス", "システム", "停止"],
      "summary": "2026年8月20日 22:00から翌1:00まで、社内ポータルサイトを停止してメンテナンスを実施します。"
    }
  ]
}  

検索ロジックの実装(search_logic.py)

検索処理は、MCPを意識しないPythonのロジックとしてsearch_logic.pyに切り出しています。

コードを見る(クリックで開閉)
def search_documents(keyword: str) -> list[dict]:
    """タイトル・本文抜粋・タグのいずれかにキーワードを含む文書を検索する。"""
    keyword_lower = keyword.lower()
    results = []
    for doc in _DOCUMENTS:
        haystack = " ".join(
            [doc["title"], doc["summary"], " ".join(doc["tags"])]
        ).lower()
        if keyword_lower in haystack:
            results.append(doc)
    return results  

他の検索ロジックとしてfilter_by_category(カテゴリ絞り込み)、filter_by_date_range(日付範囲絞り込み)、get_document(ID取得)、list_categories(カテゴリ一覧)を同じファイルに用意しています。

MCPサーバーの実装(main.py)

search_logic.py の関数を、MCPの「ツール」として公開するのがmain.pyです。
@mcp.tool()というデコレータを付けた関数が、Agentから呼び出せるツールとして公開されます。

コードを見る(クリックで開閉)

import os
from mcp.server.fastmcp import FastMCP
import search_logic

PORT = int(os.environ.get("PORT", 8080))  # Cloud Runが指定するポート番号
mcp = FastMCP(name="document-search-server", host="0.0.0.0", port=PORT)


@mcp.tool()
def search_documents(keyword: str) -> dict:
    """タイトル・本文・タグにキーワードを含む発信文書を検索する。"""
    results = search_logic.search_documents(keyword)
    return {"count": len(results), "documents": results} 

if __name__ == "__main__":
    mcp.run(transport="streamable-http")  # Cloud Run上ではHTTP経由で受けるため
  

search_logic.pyのfilter_by_category(カテゴリ絞り込み)、filter_by_date_range(日付範囲絞り込み)、get_document(詳細取得)、list_categories(カテゴリ一覧)も同様に@mcp.tool()で公開しています。

Agent(MCP Client)の実装(app.py)

次に、MCP Clientを組み込んだAgentのプログラムを作成します。

今回は画面から対話を確認するためにStreamlitを使用し、MCP Serverとは別のCloud Run (mcp-pra-agent) としてデプロイします。

なお、StreamlitはPythonだけでWebアプリを作成できるオープンソースのフレームワークです。
HTMLやJavaScriptをほとんど記述せずにチャット画面や入力フォームを実装できるため、
今回はAgentの対話画面を素早く作る目的で採用しました。

実装のポイントは以下の3点です。

  1. MCP Serverへの接続:streamablehttp_client を使い、Cloud Run上にデプロイしたMCP ServerのURL(/mcp)へ接続する
  2. ツールの変換:MCP Serverから取得したツール(list_tools)を、Geminiが理解できる FunctionDeclaration 形式に変換する
  3. ツールの実行と回答作成:Geminiからツール呼び出し(function_call)されたら、Agentが代理でMCP Serverのツールを実行(call_tool)し、Geminiに回答を作成させる
コードを見る(クリックで開閉)
import asyncio
import streamlit as st
from google import genai
from google.genai import types
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

# --- ① MCPのツール定義をGeminiのFunctionDeclaration形式に変換 ---
def mcp_tool_to_gemini_function(tool):
    return types.FunctionDeclaration(
        name=tool.name, description=tool.description or "", parameters=tool.inputSchema
    )

# --- ② 質問を受けて、Gemini呼び出しとMCPツール実行をループ処理 ---
async def ask_once(client, session, gemini_tool, contents, user_prompt):
    contents.append(types.Content(role="user", parts=[types.Part.from_text(text=user_prompt)]))

    for turn in range(5):
        # Geminiに問い合わせ(MCPから取得したツール定義を渡す)
        response = client.models.generate_content(
            model=MODEL_NAME, contents=contents,
            config=types.GenerateContentConfig(tools=[gemini_tool]),
        )
        model_content = response.candidates[0].content
        contents.append(model_content)

        # ツール呼び出し指示(function_call)がなければ最終回答として終了
        function_call_parts = [p for p in model_content.parts if p.function_call]
        if not function_call_parts:
            st.chat_message("assistant").write(response.text)
            return

        # ツール呼び出し指示がある場合、MCP Serverを実行
        for part in function_call_parts:
            fn_call = part.function_call
            # ★ MCP Clientを通じてリモートのMCP Serverのツールを呼び出す
            tool_result = await session.call_tool(fn_call.name, dict(fn_call.args))
            result_text = tool_result.content[0].text if tool_result.content else "該当なし"
            
            # 実行結果をGeminiに返すためのPartを作成
            contents.append(types.Content(role="user", parts=[
                types.Part.from_function_response(name=fn_call.name, response={"result": result_text})
            ]))

# --- ③ MCP Serverへ接続し、ツール一覧を取得して会話を開始 ---
async def process_chat(user_input):
    client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)
    
    # Cloud Run上のMCP ServerへHTTP接続
    async with streamablehttp_client(MCP_SERVER_URL) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            
            # ★ MCP Serverから利用可能なツール一覧を取得
            tools_result = await session.list_tools()
            gemini_tool = types.Tool(
                function_declarations=[mcp_tool_to_gemini_function(t) for t in tools_result.tools]
            )
            
            # Agentとのやり取りを実行
            await ask_once(client, session, gemini_tool, st.session_state.contents, user_input)

# (※以下、StreamlitのUI描画処理およびセッション管理処理は省略)

実行確認

デプロイ後、発行されたURLをブラウザで開くとチャット画面が表示され、エージェントとの対話を確認できました。

上記は「システムメンテナンスについて」と質問した際の動作結果です。

Geminiが1度の呼び出しだけではなく、 search_document(キーワード検索)→ filter_by_category(カテゴリ絞り込み)→ get_document(詳細取得)と、情報にたどり着くまでにツールを3回実行していることがわかります。

Geminiが自ら判断して複数のMCPツールを組み合わせ、正確な情報までたどり着いてから回答を作成する、MCPエージェントならではの挙動を確認できました。

つまずきポイント

最後に個人的につまずいたポイントを二つ紹介します。

① ライブラリのバージョン不整合

ModuleNotFoundError: No module named 'mcp.server.fastmcp'

MCPのPython SDKは2系でFastMCPのAPIが変更されていました。
requirements.txtに単にmcp[cli]と書くと最新の2系が入ってしまうため、mcp[cli]<2とバージョンを固定して解決しました。

/mcpパスを忘れて接続エラー

mcp.shared.exceptions.McpError: Session terminated

MCPサーバーは /mcp というパスでのみリクエストを受け付けます。デプロイ後に発行されたURLをそのまま使うと接続に失敗し、末尾に /mcp を付けることで解決しました。

まとめ

MCPとは何かという手探りの状態からスタートしましたが、実装を通してAIエージェントがどのように外部データやツールへアクセスし、連携しているのか、流れを掴むことができました。

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