日曜日, 6月 1, 2025
ホームニューステックニュースOCI生成AIサービスで指定した地域のラーメン屋情報を教えてくれるMCPサーバを作ってみた #Python - Qiita

OCI生成AIサービスで指定した地域のラーメン屋情報を教えてくれるMCPサーバを作ってみた #Python – Qiita



OCI生成AIサービスで指定した地域のラーメン屋情報を教えてくれるMCPサーバを作ってみた #Python - Qiita

MCPの勉強がてら、指定した地域のラーメン屋を教えてくれるMCPサーバを作ってみました。
例えば「つくばのラーメン屋を教えて。」といった質問を想定しています。
実装にあたり「ホットペッパーグルメ Webサービス」のAPIを利用させて頂きました。
hotpepper-s.gif

ちなみにラーメン屋を対象とした背景は、バイクでツーリングした先のラーメン屋を巡るのが趣味だからです。。

環境情報

以下サーバをOCI Computeで作成して動かしました。

  • OS: Oracle Linux 8
  • CPU: 1 OCPU
  • Memory: 16 GB

生成AIはOCI生成AIサービスに最近追加された cohere.command-a-03-2025 を使いました。

環境セットアップ

こちらのMCPチュートリアルを参考にセットアップします。
まずはPython仮想環境を作るために uv をインストールします。

$ curl -LsSf https://astral.sh/uv/install.sh | sh

仮想環境を作ります。

# 本プロジェクト用にディレクトリを新規作成
$ uv init ramen
$ cd ramen

# 仮想環境を作ってアクティベート
$ uv venv
$ source .venv/bin/activate

必要なPythonパッケージを導入します。

$ uv add "mcp[cli]" httpx langchain_community langchain_mcp_adapters langgraph oci

OCI生成AIサービスと連携するには、oci-cliのインストールも必要です。
導入手順は以下の記事がご参考になります。
Oracle Cloud : コマンド・ライン・インタフェース(CLI) をインストールしてみた

MCPサーバ実装

ホットペッパーグルメwebサービスから今回の要件に合う情報を取得するには、以下手続きを踏みます。

  1. 指定された地域のエリアコードを、エリアマスタAPI経由で取得
  2. 取得したエリアコードをグルメサーチAPIに渡し、ラーメン屋情報を取得

以上から、ツールとしては1.と2.に対応するものを用意するため、計2つのツールを作成しています。
以下コードを ramen.py というファイル名で保存します。

import os
from dotenv import load_dotenv
from typing import Annotated
import httpx
from mcp.server.fastmcp import FastMCP

# ホットペッパーグルメwebサービス利用に必要なAPIキーを.envファイルから取得
load_dotenv()
API_KEY = os.environ['API_KEY']

# FastMCPサーバを初期化
mcp = FastMCP("ramen")

# APIエンドポイント、エージェント名を設定
API_BASE = "http://webservice.recruit.co.jp/hotpepper"
USER_AGENT = "ramen-app/1.0"

# MCPサーバを定義
@mcp.tool()
async def get_small_area_code(
    area_name: Annotated[str, "小エリアコードを取得したい市区名 (例: 京都市)"]
) -> str:
    """
    リクルートWebサービスから市区名をもとに小エリアコードを取得する。
    """
    url = f"{API_BASE}/small_area/v1/?key={API_KEY}&format=json"
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/json"
    }
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            area_code = ""

            for each_area in response.json()["results"]["small_area"]:
                if area_name in each_area["name"]:
                    area_code = each_area["code"]
                    break
            return area_code

        except Exception as e:
            return f"データ取得中にエラーが発生しました: {repr(e)}"

@mcp.tool()
async def get_gourmet_info(
        small_area_code: Annotated[str, "ラーメン屋の情報を取得したい小エリアのコード (例: X010)"]
) -> str:
    """
    リクルートWebサービスから小エリアコードをもとに指定地域のラーメン屋情報を取得する。
    """
    url = f"{API_BASE}/gourmet/v1/?key={API_KEY}&small_area={small_area_code}&genre=G013&format=json"
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/json"
    }
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            shop_info_list = ""

            for each_shop in response.json()["results"]["shop"]:
                shop_info_list = shop_info_list + f"""
店名: {each_shop["name"]} {each_shop["name_kana"]}
特徴: {each_shop["catch"]}

"""
            return shop_info_list

        except Exception as e:
            return f"データ取得中にエラーが発生しました: {repr(e)}"
    
if __name__ == "__main__":
    mcp.run(transport="stdio")

MCPクライアント実装

MCPクライアント実装にあたり、以下記事を参考にさせて頂きました。
MCPで変わるAIエージェント開発 – MCPクライアントのコード

以下コードを client.py というファイル名で保存します。
ちなみに入力プロンプトをハードコーディングしており、次の質問を投げています。

“つくばのラーメン屋を教えて下さい。”

import asyncio
import oci
from langchain_community.chat_models import ChatOCIGenAI
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent

# OCI認証情報の定義
config = oci.config.from_file("~/.oci/config", "DEFAULT")

# 連携するLLMの定義
model = ChatOCIGenAI(
    model_id="cohere.command-a-03-2025",
    service_endpoint="https://inference.generativeai.ap-osaka-1.oci.oraclecloud.com",
    compartment_id="ocid1.compartment.oc1..aaaaaaaav7mpd2hotl6vxostu6nkpjmdr3tdghtp523mwjgsxemylso4w2qa",
    model_kwargs={"temperature": 0.7, "max_tokens": 500}
)

async def main():
   # MCPサーバーの定義
    client = MultiServerMCPClient(
        {
            "ramen": {
                "command": "python",
                "args": ["/home/opc/ramen/ramen.py"],
                "transport": "stdio",
            }
        }
    )

    # MCPサーバーをツールとして定義
    tools = await client.get_tools()

    # エージェントの定義
    agent = create_react_agent(model, tools)

    # 入力プロンプトの定義
    agent_response = await agent.ainvoke({
        "messages": "つくばのラーメン屋を教えて下さい。"
    })

    # 出力結果の表示
    messages = agent_response.get("messages")
    for each_message in messages:
        # メッセージタイプを出力
        print(f"\n--- {type(each_message).__name__} ---")
        # メッセージ本文を出力
        print(each_message)

# エージェントの実行
if __name__ == "__main__":
    asyncio.run(main())

実行してみる

MCPクライアントを以下のように実行します。

結果は以下のようになります。
※主要な出力のみ抜粋した上で整形しています。

--- HumanMessage ---
content="つくばのラーメン屋を教えて下さい。"

--- AIMessage ---
content="まず、つくばのラーメン屋を検索するために、つくばの「小エリアコード」を取得します。その後、そのコードを用いてラーメン屋情報を検索します。...
tool_calls=[{"name': 'get_small_area_code', 'args': {'area_name': 'つくば'}, 'id': '25379111eba44ea492970dca4ed241f5', 'type': 'tool_call'}]

--- ToolMessage ---
content="X587" name="get_small_area_code" id='d95115c7-ebd7-47b9-bce2-6b6f69dd429a' tool_call_id='25379111eba44ea492970dca4ed241f5'

--- AIMessage ---
content="つくばの小エリアコードはX587でした。次に、このコードを用いてラーメン屋情報を検索します。...
tool_calls=[{"name': 'get_gourmet_info', 'args': {'small_area_code': 'X587'}, 'id': 'c44e5614981742359bd8ba0d5ecce0d2', 'type': 'tool_call'}]

--- ToolMessage ---
content="\n店名: 山岡家 つくば中央店\u3000やまおかや\u3000つくばちゅうおうてん\n特徴: \n\n\n店名: 天下一品 つくば店\u3000てんかいっぴん\u3000つくばてん\n特徴: TVでおなじみの店 超濃厚こってりスープ\n\n\n店名: 丸源ラーメン つくば店\u3000まるげんらーめん\u3000つくばてん\n特徴: イベント毎日開催中☆ 女性一人でも入りやすい店\n\n\n店名: 龍郎\u3000たつろう\n特徴: 安い!! 野菜増し無料!\n\n\n店名: 活龍\u3000かつりゅう\n特徴: 龍神ぎょうざ 人気!とんこつ醤油\n\n" name="get_gourmet_info" id='106cc1d4-39ec-4ef1-8756-368789906dd2' tool_call_id='c44e5614981742359bd8ba0d5ecce0d2'

--- AIMessage ---
content="以下のラーメン屋がつくばにあります。\n\n- 山岡家 つくば中央店\n- 天下一品 つくば店\n- 丸源ラーメン つくば店\n- 龍郎\n- 活龍"
additional_kwargs={'documents': [{'id': 'get_gourmet_info:0:3:0', 'output': '\n店名: 山岡家 つくば中央店\u3000やまおかや\u3000つくばちゅうおうてん\n特徴: \n\n\n店名: 天下一品 つくば店\u3000てんかいっぴん\u3000つくばてん\n特徴: TVでおなじみの店 超濃厚こってりスープ\n\n\n店名: 丸源ラーメン つくば店\u3000まるげんらーめん\u3000つくばて ん\n特徴: イベント毎日開催中☆ 女性一人でも入りやすい店\n\n\n店名: 龍郎\u3000たつろう\n特徴: 安い!! 野菜増し無料!\n\n\n店名: 活龍\u3000かつりゅう\n特徴: 龍神ぎょうざ 人気!とんこつ醤油\n\n'}]

以上の通り、AIエージェントが自律的に適切なツールを適切な順番で呼び出し、最終的に欲しい回答を導き出している様子が分かります。
結果を見て実際に使えそうな印象を受けましたので、今度ツーリングする際は情報収集に使おうと思いました。

またMCPサーバは今回の方法で手軽に追加できそうなため、色々と機能拡張を試したいです。





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -