[MCP]ノンプログラマーのためのMCP入門 MCPサーバーを自作しよう!後編

mcp6MCP

どうも、ケニー(tsujikenzo)です。このシリーズでは、「ノンプログラマーのためのMCP入門」をお届けします。今日は第6回目で、いよいよ最終回です。

おさらい

前回は、自分のPC内のテキストファイルをAIに読み取らせる、自作MCPサーバーを作りました。

最終回となる今回は、あなたが作ったMCPサーバーをインターネット上に公開する方法、そして、MCPサーバーを理解したあなたの、労働市場での価値についてお話しします。

知っておきたい「4つの設計パターン」

前回より複雑なMCPサーバーの自作を始める前に、MCPサーバーにはどのような「作り方」があるのか、その代表的な4つのパターンを押さえておきましょう。

  1. シンプルラッパーパターン
    既存のAPIを、MCPのプロトコルで包む(ラップする)だけの、最も基本的で強力な形です。
  2. カスケード処理パターン
    1つの命令で、複数のAPIやツールを連鎖的に呼び出し、複雑な結果を出す形です。
  3. ナレッジベースパターン
    社内文書や膨大なデータを「リソース」として提供し、AIの知識を補強する形です。
  4. ステートフル vs ステートレス
    やり取りの記憶を保持する(ステートフル)か、1回ごとに完結させる(ステートレス)かの設計思想です。

Yahoo!ローカルサーチAPI

Yahoo!は、店舗・施設・イベントなどの地域・拠点情報(POI)をキーワードや位置情報で簡単に検索できるAPIを、無料で公開しています。

Yahoo!ローカルサーチAPI - Yahoo!デベロッパーネットワーク
Yahoo! Open Local PlatformのローカルサーチAPIを使うと、店舗、イベント、クチコミ情報などの地域・拠点情報(POI)を検索できます。

今回、自作するMCPサーバーは、この「Yahoo!ローカルサーチAPI」をラップする、シンプルラッパーパターンのMCPサーバーです。

名付けて「Yahoo!ローカルサーチ MCP(yahoo-local-search-mcp))」です。

クライアントシークレットとクライアントIDの取得

Yahoo!ローカルサーチAPIを使うには、Yahoo! JAPAN IDを取得して、アプリケーションを登録して、Client IDの発行が必要です。事前にご準備ください。

今回は、紙面の都合上割愛させていただきます。ご了承ください。

ご利用ガイド - Yahoo!デベロッパーネットワーク
Yahoo!デベロッパーネットワークで公開しているAPIを使って開発を始めるために必要な手続きをご紹介します。

取得したClient IDは、メモ帳にコピペしておきましょう。

MCPサーバーを実装する

前回のおさらいも含めて、MCPサーバーを自作していきましょう。

ステップ①:下準備

  1. ローカル(今回もデスクトップ)に「yahoo-local-search-mcp」フォルダを作成します。
  2. ターミナルでカレントディレクトリを移動します。cd
  3. uvを初期化します。uv init
  4. 開発キット(FastMCP)をインストールします。uv add fastmcp

ステップ②:MCPサーバーの実装

MCPサーバーの中身のプログラム(app.py)は、前回同様に、生成AIに書かせます。

Yahoo!ローカルサーチAPIをラップする、実務特化型のMCPサーバー `app.py` をPythonのFastMCPで作ってください。

## 公式リファレンス
https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/localsearch.html

## 設計方針
- シンプルラッパーパターンで作ってください。公式APIのリクエストパラメータをそのままMCPツールの引数に1対1で対応付けます。独自の抽象化や正規化レイヤーは挟まないでください。
- 認証はClient ID(`appid`パラメータ)のみ。環境変数 `YAHOO_APP_ID` から取得してください。
- レスポンスはJSON固定にしてください(Yahoo!のデフォルトはXMLなので、必ず `output=json` をリクエスト時に付与する)。

## 提供するMCPツール(3つ)

### 1. `search_local` - 汎用検索ツール
公式APIの全パラメータを公開する汎用ツール。以下のリクエストパラメータをすべて引数として受け取れるようにしてください:

- `query`: 検索キーワード
- `gc`: 業種コード
- `ac`: 住所コード(JIS X 0401、都道府県2桁または市区町村5桁)
- `lat`, `lon`: 中心緯度経度
- `dist`: 検索半径km(最大20)
- `bbox`: 矩形範囲
- `sort`: ソート順(rating, score, hybrid, review, kana, price, dist, geo, match。"-"プレフィックスで降順)
- `start`: 取得開始位置(最大3000)
- `results`: 取得件数(最大100)
- `detail`: simple / standard / full
- `cid`: カセットID
- `open_param`: 営業時間フィルタ(now, "5,19", "Mon,12" 形式。Pythonの `open` 予約語回避のため引数名は `open_param` にしてAPI送信時に `open` にリネーム)
- `coupon`, `parking`, `creditcard`, `image`: bool(true/falseの文字列に変換してAPI送信)
- `smoking`: 喫煙可否
- `reservation`: "1"で予約可能
- `maxprice`, `minprice`: 価格帯
- `group`, `distinct`: 名寄せ関連

### 2. `search_by_genre` - 業種コード軸の検索ラッパー
`search_local` の薄いラッパー。`gc`(業種コード)を必須にして、よく使う業種での絞り込みに特化。電話帳カセット(`d8a23e9e64a4c817227ab09858bc1330`)をデフォルトで使用するオプション `use_phonebook=True` を持たせる。

### 3. `search_nearby` - 周辺検索ラッパー
`search_local` の薄いラッパー。`lat` / `lon` / `dist` を必須にして、距離順ソート(`sort=geo`)をデフォルト値に。電話帳カセットをデフォルトで使用。

## バリデーション要件
APIに無駄なリクエストを投げないよう、ローカルで以下を検証してください:

- `appid` 以外に `query` / `gc` / `ac` / `cid` / `lat+lon` のいずれかが指定されていること(仕様上の必須条件)
- `dist` を指定する場合は `lat` と `lon` も必須
- `dist` は 0 < dist <= 20
- `results` は 1〜100
- `start` は 1〜3000
- `sort` は規定値のいずれか
- `detail` は simple / standard / full のいずれか

## 環境変数

| 変数名 | 必須 | 説明 |
|---|---|---|
| `YAHOO_APP_ID` | ✅ | Client ID(アプリケーションID) |
| `YAHOO_LOCALSEARCH_BASE` | – | APIベースURL(既定: `https://map.yahooapis.jp`) |

## 実装上の注意
- `httpx.Client` でGETリクエスト
- タイムアウト30秒
- HTTPステータス4xx以上はレスポンス本文を含めて `RuntimeError` を投げる
- bool型のリクエストパラメータは `"true"` / `"false"` 文字列に変換してから送信
- `None` の引数はリクエストパラメータから除外
- レスポンスJSONをそのままdictで返す(YDF構造の解釈は呼び出し側に任せる)

## エンドポイント情報
- ベースURL: `https://map.yahooapis.jp`
- パス: `/search/local/V1/localSearch`
- HTTPメソッド: GET のみ
- レート制限: 1アプリケーション/24時間 = 50,000リクエスト

## クライアント設定例(参考)
```json
{
  "mcpServers": {
    "mcp-yahoo-localsearch": {
      "command": "uv",
      "args": [
        "--directory",
        "C:/Users/あなたのお名前/Desktop/mcp-yahoo-localsearch",
        "run",
        "python",
        "app.py"
      ],
      "env": {
        "YAHOO_APP_ID": "<取得したClient ID>"
      }
    }
  }
}
```

`pyproject.toml` も合わせて出力してください。依存関係は `mcp[cli]>=1.2.0` と `httpx>=0.27.0`。Python 3.10以上対応。

出力された、MCPサーバー本体のapp.pyはこちらです。

 """
mcp-yahoo-localsearch
=====================

Yahoo!ローカルサーチAPI (YOLP) の MCP サーバー (シンプルラッパーパターン)。

公式仕様:
    https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/localsearch.html

提供ツール:
    - search_local       : キーワード/業種/住所/緯度経度などで地域・拠点情報を検索
    - search_by_genre    : 業種コードを軸にした検索 (search_localの薄いラッパー)
    - search_nearby      : 中心座標+半径での円範囲検索 (search_localの薄いラッパー)

エンドポイント:
    GET https://map.yahooapis.jp/search/local/V1/localSearch

レスポンス形式:
    JSON 固定 (output=json をAPI側に強制送信)。
    Yahoo!の仕様上 XML がデフォルトのため、必ず明示する。

レート制限:
    1アプリケーション/24時間 = 50,000リクエスト

環境変数:
    YAHOO_APP_ID : Client ID (アプリケーションID, 必須)
"""

from __future__ import annotations

import os
from typing import Any, Optional

import httpx
from mcp.server.fastmcp import FastMCP


# --------------------------------------------------------------------------- #
# 設定
# --------------------------------------------------------------------------- #

APP_ID = os.environ.get("YAHOO_APP_ID", "")
API_BASE = os.environ.get(
    "YAHOO_LOCALSEARCH_BASE",
    "https://map.yahooapis.jp",
).rstrip("/")
ENDPOINT = "/search/local/V1/localSearch"

HTTP_TIMEOUT = 30.0

# 電話帳カセットID (公式仕様書記載)。最も網羅性が高い汎用カセット。
PHONEBOOK_CASSETTE_ID = "d8a23e9e64a4c817227ab09858bc1330"


# --------------------------------------------------------------------------- #
# 共通ユーティリティ
# --------------------------------------------------------------------------- #

def _ensure_credentials() -> None:
    if not APP_ID:
        raise RuntimeError(
            "環境変数 YAHOO_APP_ID が未設定です。"
            "Yahoo!デベロッパーネットワークで取得した Client ID を設定してください。"
        )


def _request(params: dict[str, Any]) -> dict[str, Any]:
    """ローカルサーチAPIへ GET リクエスト送信、JSON を返す。"""
    _ensure_credentials()

    # 必須・固定パラメータ
    params = {k: v for k, v in params.items() if v is not None and v != ""}
    params["appid"] = APP_ID
    params["output"] = "json"

    url = f"{API_BASE}{ENDPOINT}"

    try:
        with httpx.Client(timeout=HTTP_TIMEOUT) as client:
            response = client.get(url, params=params)
    except httpx.HTTPError as exc:
        raise RuntimeError(f"APIリクエスト時にネットワークエラーが発生しました: {exc}") from exc

    if response.status_code >= 400:
        try:
            body: Any = response.json()
        except ValueError:
            body = response.text
        raise RuntimeError(
            f"API呼び出しが失敗しました (HTTP {response.status_code}): {body}"
        )

    try:
        return response.json()
    except ValueError as exc:
        raise RuntimeError(
            f"APIレスポンスのJSONパースに失敗しました: {response.text[:500]}"
        ) from exc


# --------------------------------------------------------------------------- #
# MCP サーバー
# --------------------------------------------------------------------------- #

mcp = FastMCP("mcp-yahoo-localsearch")


@mcp.tool()
def search_local(
    query: Optional[str] = None,
    gc: Optional[str] = None,
    ac: Optional[str] = None,
    lat: Optional[float] = None,
    lon: Optional[float] = None,
    dist: Optional[float] = None,
    bbox: Optional[str] = None,
    sort: str = "hybrid",
    start: int = 1,
    results: int = 20,
    detail: str = "standard",
    cid: Optional[str] = None,
    open_param: Optional[str] = None,
    coupon: Optional[bool] = None,
    parking: Optional[bool] = None,
    creditcard: Optional[bool] = None,
    smoking: Optional[str] = None,
    reservation: Optional[str] = None,
    image: Optional[bool] = None,
    maxprice: Optional[int] = None,
    minprice: Optional[int] = None,
    group: Optional[str] = None,
    distinct: Optional[bool] = None,
) -> dict[str, Any]:
    """Yahoo!ローカルサーチAPIで店舗・施設情報を検索する汎用ツール。

    キーワード・業種コード・住所コード・緯度経度・矩形範囲などを組み合わせて、
    全国の店舗データ(電話帳カセットなど)を検索する。

    仕様上の必須条件:
        appid 以外に、query / gc / ac / lat+lon / cid のいずれかを必ず指定すること。
        いずれも未指定の場合は API 側で 400 エラーになる。

    Args:
        query: 検索キーワード (例: "焼肉", "イオン", "サントリー美術館")
        gc: 業種コード (例: "0118008"=焼肉, "0118"=韓国・朝鮮料理, "0202"=スーパーマーケット)
            YOLP業種コード一覧:
            https://developer.yahoo.co.jp/webapi/map/openlocalplatform/genre.html
        ac: 住所コード (JIS X 0401)
            都道府県は2桁 (例: "01"=北海道, "13"=東京都)
            市区町村は5桁 (例: "01101"=札幌市中央区, "13103"=東京都港区)
            国コード (ISO 3166-1 alpha-2) も指定可能 (例: "JP", "US")
        lat: 中心緯度 (世界測地系、例: 43.0644 = 札幌駅)
        lon: 中心経度 (例: 141.3470 = 札幌駅)
        dist: 検索距離 km (最大20、小数可)。lat/lonと併用必須。
        bbox: 矩形範囲。"左下経度,左下緯度,右上経度,右上緯度" 形式。
        sort: ソート順
            - "rating": 星の数順
            - "score": スコア順
            - "hybrid": 適合度・口コミ・評価のハイブリッド (デフォルト)
            - "review": 口コミ件数順
            - "kana": あいうえお順
            - "price": 金額順
            - "dist": 直線距離順 (lat/lon必須、geoより高速)
            - "geo": 球面三角法による距離順 (lat/lon必須、より正確)
            - "match": 適合度順
            "-rating" のように "-" を先頭に付けると降順。例: sort="-review"
        start: 取得開始位置 (1始まり、最大3000)
        results: 取得件数 (最大100)
        detail: 出力項目数
            - "simple": 最小限
            - "standard": 標準 (デフォルト)
            - "full": 全項目 (写真URL等含む)
        cid: カセットID。電話帳カセット = "d8a23e9e64a4c817227ab09858bc1330"
            未指定時は公開カセット全体が対象。
        open_param: 営業時間フィルタ
            - "now": 現在時刻に開店している施設
            - "5,19": 5日の19時に開店 (日付,時間)
            - "Mon,12": 月曜12時に開店 (曜日,時間。曜日はMon〜Sun)
        coupon: trueでクーポン利用可の店舗のみ
        parking: trueで駐車場ありの店舗のみ
        creditcard: trueでクレジットカード利用可の店舗のみ
        smoking: 喫煙可否 ("1"=禁煙, "2"=分煙, "3"=喫煙可、カンマ区切りで複数指定)
        reservation: "1" で予約可能な店舗のみ
        image: trueで画像があるデータのみ
        maxprice: 価格帯上限
        minprice: 価格帯下限
        group: "gid" を指定で同一店舗を名寄せ
        distinct: group=gid 時に false で重複レコード全表示

    Returns:
        APIレスポンス (YDF形式のJSON)。ResultInfo, Feature[] を含む dict。
    """
    # 必須条件チェック (APIに無駄なリクエストを投げる前に弾く)
    if not any([query, gc, ac, cid, (lat is not None and lon is not None)]):
        raise ValueError(
            "検索条件が不足しています。query / gc / ac / cid / (lat+lon) "
            "のいずれかを指定してください。"
        )

    if dist is not None and (lat is None or lon is None):
        raise ValueError("dist を指定する場合は lat と lon も必須です。")
    if dist is not None and not (0 < dist <= 20):
        raise ValueError("dist は 0 より大きく 20 以下で指定してください (km)。")

    if not (1 <= results <= 100):
        raise ValueError("results は 1 から 100 の範囲で指定してください。")
    if not (1 <= start <= 3000):
        raise ValueError("start は 1 から 3000 の範囲で指定してください。")

    valid_sorts = {"rating", "score", "hybrid", "review", "kana",
                   "price", "dist", "geo", "match"}
    sort_key = sort.lstrip("-")
    if sort_key not in valid_sorts:
        raise ValueError(
            f"sort は {sorted(valid_sorts)} のいずれか (任意で先頭に '-') を指定してください。"
        )

    if detail not in {"simple", "standard", "full"}:
        raise ValueError("detail は simple / standard / full のいずれかを指定してください。")

    # bool は Yahoo API では "true"/"false" 文字列を期待
    def _bool(v: Optional[bool]) -> Optional[str]:
        if v is None:
            return None
        return "true" if v else "false"

    params: dict[str, Any] = {
        "query": query,
        "gc": gc,
        "ac": ac,
        "lat": lat,
        "lon": lon,
        "dist": dist,
        "bbox": bbox,
        "sort": sort,
        "start": start,
        "results": results,
        "detail": detail,
        "cid": cid,
        "open": open_param,
        "coupon": _bool(coupon),
        "parking": _bool(parking),
        "creditcard": _bool(creditcard),
        "smoking": smoking,
        "reservation": reservation,
        "image": _bool(image),
        "maxprice": maxprice,
        "minprice": minprice,
        "group": group,
        "distinct": _bool(distinct),
    }

    return _request(params)


@mcp.tool()
def search_by_genre(
    gc: str,
    ac: Optional[str] = None,
    query: Optional[str] = None,
    lat: Optional[float] = None,
    lon: Optional[float] = None,
    dist: Optional[float] = None,
    sort: str = "hybrid",
    results: int = 20,
    detail: str = "standard",
    use_phonebook: bool = True,
) -> dict[str, Any]:
    """業種コード (gc) を軸に店舗を検索 (search_local の薄いラッパー)。

    YOLP業種コードの代表例:
        - "01"          : グルメ全般
        - "0118008"     : 焼肉/ホルモン
        - "0117001"     : 寿司
        - "01170011"    : 回転寿司
        - "0107001"     : 居酒屋
        - "0202"        : スーパーマーケット
        - "020201"      : コンビニエンスストア
        - "0203001"     : 百貨店・デパート
        - "030201"      : ホテル
    完全な一覧:
        https://developer.yahoo.co.jp/webapi/map/openlocalplatform/genre.html

    Args:
        gc: 業種コード (必須)
        ac: 住所コードで地域を絞り込む (例: "01"=北海道, "01101"=札幌市中央区)
        query: 追加のキーワード絞り込み (例: 業種=焼肉 + query="ジンギスカン")
        lat, lon, dist: 緯度経度+半径km (距離順検索したい時に有効)
        sort: ソート順 (デフォルト "hybrid"。距離順なら "geo")
        results: 取得件数 (最大100)
        detail: simple / standard / full
        use_phonebook: True で電話帳カセットに限定する (デフォルト True、データ品質安定)

    Returns:
        APIレスポンス (YDF形式のJSON)。
    """
    cid = PHONEBOOK_CASSETTE_ID if use_phonebook else None
    return search_local(
        query=query,
        gc=gc,
        ac=ac,
        lat=lat,
        lon=lon,
        dist=dist,
        sort=sort,
        results=results,
        detail=detail,
        cid=cid,
    )


@mcp.tool()
def search_nearby(
    lat: float,
    lon: float,
    dist: float = 1.0,
    query: Optional[str] = None,
    gc: Optional[str] = None,
    sort: str = "geo",
    results: int = 20,
    detail: str = "standard",
    use_phonebook: bool = True,
) -> dict[str, Any]:
    """指定緯度経度の中心から半径dist km以内の店舗を検索 (search_localの薄いラッパー)。

    主な用途:
        - 取引先住所をジオコーディングした後、その周辺の競合店・関連店舗を発掘
        - 営業エリアの店舗密度調査
        - イベント会場 (例: Food Taipei 会場) 周辺の飲食店リスト化

    Args:
        lat: 中心緯度 (世界測地系)
        lon: 中心経度
        dist: 検索半径 km (デフォルト1.0、最大20.0)
        query: キーワード絞り込み (例: "ラーメン")
        gc: 業種コード絞り込み (例: "0118008"=焼肉)
        sort: ソート順 (デフォルト "geo" = 距離順)
        results: 取得件数 (最大100)
        detail: simple / standard / full
        use_phonebook: True で電話帳カセットに限定 (デフォルト True)

    Returns:
        APIレスポンス (YDF形式のJSON)。
    """
    cid = PHONEBOOK_CASSETTE_ID if use_phonebook else None
    return search_local(
        query=query,
        gc=gc,
        lat=lat,
        lon=lon,
        dist=dist,
        sort=sort,
        results=results,
        detail=detail,
        cid=cid,
    )


# --------------------------------------------------------------------------- #
# エントリポイント
# --------------------------------------------------------------------------- #

if __name__ == "__main__":
    mcp.run()

フォルダ内にapp.pyファイルを作成して、保存しましょう。

ステップ③:構成ファイル(JSON)を「uv方式」で書く

最後に構成ファイルです。claude_desktop_config.json を開き、プロパティ「mcpServers」の中に以下の設定を記述します。

"mcpServers": {
    "yahoo-local-search-mcp": {
      "command": "uv",
      "args": [
        "--directory",
        "C:/Users/あなたの名前/Desktop/mcp-yahoo-localsearch",
        "run",
        "python",
        "app.py"
      ],
      "env": {
        "YAHOO_APP_ID": "<Yahoo!デベロッパーネットワークで取得したClient ID>"
   }
 }

これは、第4回で学んだ「認証情報の渡し方 3パターン」のうち、環境変数型を採用しています。

  1. 認証不要型: 公開情報などを扱う際の手ぶらスタイル。
  2. 環境変数(env)型: 【推奨】設定ファイルに鍵を隠し持つ、最も安全なお作法。
  3. 引数(args)型: 起動コマンドに直接書く、手軽だが丸見えのテスト用。

更新して保存したら、Claude desktopを再起動しましょう。

MCPサーバーが追加されたら、準備OKです。

接続を確認する

それでは、チャット欄でこう話しかけてみてください。汎用的な質問のばあい、MCPサーバー名を追加した方が良さそうです。

プロンプト:yahoo-local-search-mcpで「サントリー美術館」を検索して

MCPサーバーの読み込み許可が確認できると、なお確実です。

いい感じの出力を得られました。

サントリー美術館の検索結果が出ました。以下が主な検索結果です:

サントリー美術館

📍 基本情報

  • 住所:東京都港区赤坂9-7-4(東京ミッドタウン内)
  • 電話:03-3479-8600
  • アクセス
    • 六本木駅(都営大江戸線/東京メトロ日比谷線):出口7から約396m(徒歩4分)
    • 乃木坂駅(東京メトロ千代田線):出口2から約440m(徒歩5分)

🏛️ 館の特徴

建築家・隈研吾氏がデザイン。「都市の居間」をコンセプトに建てられており、以下の特徴があります:

  • 白磁のルーバー(縦格子)に覆われた外観
  • 館内は木と和紙を意匠に使用
  • 日本の伝統と現代を融合させた「和のモダン」を基調
  • 安らぎと優しさに溢れた空間

⏰ 営業時間

  • 月~木、日:10:00~18:00
  • 金・土:10:00~20:00
  • 定休日:毎週火曜、年末年始、展示替期間

💰 入館料

  • 展示会により異なります
  • 中学生以下は無料

🎨 展示

企画展のみで、年5~6回開催されています。

🏪 施設

  • 売店:あり
  • コインロッカー:あり
  • ベビー用施設:あり
  • トイレ:あり(障害者優先トイレあり)
  • バリアフリー対応

検索結果には美術館本体のほか、館内に入っているカフェ「サントリー美術館 shop×cafe」も含まれています。

もう少し突っ込んだ指示を出してみましょう。

yahoo-local-search-mcpで札幌市の焼肉店を口コミ多い順に30件取得して、
店舗名・住所・電話番号・最寄駅を表にしてCSVで出力して

いい感じでcsvファイルが出力できました。

公開する

最後に公開です。自作MCPサーバーを、uvx方式で誰でも使えるようにしましょう。

ステップ①:パッケージ名の重複チェック

PyPI で同じ名前のパッケージが公開されていないか確認します。確認方法は、ブラウザにURLを入力するだけです。

# ブラウザで以下にアクセス、404が出れば名前が空いている
# https://pypi.org/project/{あなたの作成したMCPサーバー名}/
例: https://pypi.org/project/yahoo-local-search-mcp/

ステップ②:pyproject.toml を「公開用」に書き直す

プロジェクトフォルダ内の、pyproject.tomlファイルを書き換えます。

[project]
name = "yahoo-local-search-mcp"
version = "0.1.0"
description = "MCP server wrapping Yahoo! Local Search API (YOLP)"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
    {name = "Kenny", email = "あなたのメールアドレス"}
]
keywords = ["mcp", "yahoo", "yolp", "localsearch", "japan"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = [
    "mcp[cli]>=1.2.0",
    "httpx>=0.27.0",
]

# ★ ここが重要:uvx で起動するためのエントリーポイント
[project.scripts]
yahoo-local-search-mcp = "app:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["."]
include = ["app.py"]

ステップ③:app.py に main() 関数を追加

uvx でコマンド起動するには、pyproject.toml [project.scripts] から呼ばれる関数が必要です。

app.pyの最下部に追加して、上書き保存しましょう。

def main() -> None:
    """Entry point for uvx execution."""
    mcp.run()


if __name__ == "__main__":
    main()

ステップ④:PyPIアカウント作成とAPI tokenの発行

https://pypi.org/account/register/ でアカウントを作成します。

API tokenを発行して、メモしておきます。

ステップ⑤:公開する

API tokenを使って公開します。

まず、ターミナルを起動して、カレントディレクトリを移動します。

cd C:\Users\Kenzo\Desktop\yahoo-local-search-mcp

次に、パッケージ(uv)をビルドします。

uv build

先ほどのトークンをつかって、公開(publish)します。

uv publish --token {pypiから始まるあなたのトークン}

ステップ⑥:確認する

公開後数分待ってから、以下のコマンドで PyPI からの起動をテストします。

uvx yahoo-local-search-mcp

インストールが完了すれば成功です。

社内でMCPサーバーを共有したり、別のClaude desktopから接続してみましょう。

まとめ

以上で、「MCPサーバーを自作しよう!後編」をお届けしました。

今後、インターネット上にあるさまざまな情報のインターフェイスは、AIに置き換えられていきます。

APIを自作して公開することは、ノンプログラマーにとって少しハードルが高いと思います。

しかし、すでにAPIが公開されているサービスなどを、MCPサーバー化することは、需要が高いはずです。

なんでもかんでもMCP化すればいいというものではありませんが、痒い所に手が届くニーズにこたえるような中小企業や個人事業主などの課題解決に、もってこいだと思います。

楽天RMS APIのMCPサーバーを自作して、Claude Codeから商品管理を全自動化した話|ユーファス(UFAS)
ShopifyがMCPネイティブ対応を全店舗に展開した。楽天は? まだない。 だから自分で作った。 この記事では、楽天RMS APIをMCP(Model Context Protocol)サーバー化して、Claude Codeから自然言語で商品管理を操作できるようにした実装の全貌を紹介します。 そもそもMCPとは...
SUZURI APIを使ってSUZURI MCP Serverを作った話 / 開発者向けブログ・イベント | GMO Developers
GMOインターネットグループエキスパート黒瀧です。今回は、SUZURIの公開APIを活用してMCP(Model Context Protocol)サーバーを作った話を紹介します。

みなさんの公開したMCPサーバーを教えてくださいね。たのしみにしています!

最後までお読みいただき、ありがとうございました。

参考資料

このシリーズの目次

  1. [MCP]はじめに(MCPとは)
  2. [MCP]骨格と用語(クライアント/サーバー、ツール等)
  3. [MCP]環境と構成ファイル(どうやってつながるのか)
  4. [MCP]MCPサーバーを接続してみよう!(ハンズオン)
  5. [MCP]MCPサーバーを自作しよう!前編
  6. [MCP]MCPサーバーを自作しよう!後編

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