MCPの勉強がてら、指定した地域のラーメン屋を教えてくれるMCPサーバを作ってみました。
例えば「つくばのラーメン屋を教えて。」といった質問を想定しています。
実装にあたり「ホットペッパーグルメ Webサービス」のAPIを利用させて頂きました。
ちなみにラーメン屋を対象とした背景は、バイクでツーリングした先のラーメン屋を巡るのが趣味だからです。。
環境情報
以下サーバを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サービスから今回の要件に合う情報を取得するには、以下手続きを踏みます。
- 指定された地域のエリアコードを、エリアマスタAPI経由で取得
- 取得したエリアコードをグルメサーチ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サーバは今回の方法で手軽に追加できそうなため、色々と機能拡張を試したいです。
Views: 0