F2T相談してみる
AI・業務自動化

広告が夜中に止まる問題を Claude Code MCP で解決した話

広告が夜中に止まる問題を Claude Code MCP で解決した話

月100万円の広告費を使っていて、20台のサーバーを毎朝自動でチェックし、トークン切れを事前に検知する仕組みを月2,600円($20)で作った。構築にかかった時間は筆者の場合で約3時間。この記事では、そのシステムの全容と作り方を、エンジニアでなくても理解できる言葉で書く。

朝起きたら広告が止まっていた、という話

夜中にアクセストークンが切れるという罠

ある月曜の朝、Google 広告の管理画面を開いたら数値が更新されていなかった。前日の日曜深夜、Meta広告のアクセストークン(APIにログインするための「合言葉」のようなもの)が期限切れになっていた。

広告自体は配信され続けていた。止まっていたのは「データの取得」だ。自動レポートが壊れ、入札の自動調整が止まり、異常検知も機能していなかった。日曜の夜から月曜の朝まで、約12時間。管理画面を開くまで誰も気づかなかった。

代理店も気づいていなかった60日ルール

Meta の Long-lived User Token には約60日の有効期限がある(Meta公式ドキュメント)。2ヶ月に一度、手動で更新しなければトークンは失効する。

代理店に確認したところ、「手動で更新しています」とのこと。人の記憶に頼る運用は、誰がやっても漏れが出る。実際、n8nのコミュニティでもこのトークン更新の自動化は頻繁に議論されている。筆者だけの問題ではない。

ちなみに System User Token なら無期限で発行できる。ただしBusiness Manager上での設定が必要で、Marketing APIのStandard access以上の権限も要る。知らなければ60日の罠にはまり続ける。

「これ、自分で監視できないのか?」

そこで調べ始めた。

広告監視の専用SaaSはある。Supermetrics は$37/月から(年間契約のみ、月払い不可。公式価格)。Improvado は公式価格非公開だが、G2やCapterraなどのレビューサイトでは月額$3,000〜$10,000の範囲が多い(正確な価格は問い合わせベース)。

中小企業には重い。月100万円の広告費に対して、監視だけで月数千ドルは割に合わない。

もっと安く、自分でコントロールできる方法はないか。たどり着いたのが Claude Code と MCP の組み合わせだった。

Claude Code と MCP を経営者向けに翻訳すると

Claude Code は「ターミナル上の優秀な助手」

Claude Code は Anthropic 社が提供するAIツールだ。パソコンの「ターミナル」(黒い画面にコマンドを打つあれ)の上で動く。

普通のAIチャットとの違いは、実際にファイルを読み書きしたり、コマンドを実行したりできること。「この広告アカウントの状態を確認して」と頼めば、APIに接続して結果を返してくれる。

2026年4月時点で、Mac・Windows・Linux に対応。VS Code やJetBrains の拡張、デスクトップアプリ、Webアプリ(claude.ai/code)と、5つの使い方がある(Claude Code公式ドキュメント)。

料金は Pro プランの月額$20(約2,600円)から使える(Claude公式価格ページ)。

MCP は「各サービスの共通語」

MCP(Model Context Protocol)は、AIが外部サービスと会話するための「共通語」だ。

たとえば電源プラグ。日本とアメリカでは形が違う。でも変換アダプタがあればどちらのコンセントにも挿せる。MCPはAIの世界の変換アダプタで、Google広告、Meta広告、Slack、スプレッドシートなど、別々のサービスをひとつの「言語」でつなぐ。

2024年11月にAnthropicが発表し、その後 Linux Foundation 傘下の Agentic AI Foundation(AAIF)に寄贈された。OpenAI、Google、Microsoft、Amazon もAAIFに参画している(Wikipedia - Model Context Protocol)。一社だけの技術ではなく、業界標準になりつつある。

2026年3月末時点で、MCPの公式レジストリには8,013のサーバー(接続先)が登録されている(Skills Index)。

2026年4月時点で使えるようになっていること

広告運用に関係するMCPサーバーをまとめると、こうなる。

サービス

MCP サーバー

できること

制限

Google Ads

Google 公式(googleads/google-ads-mcp)

レポート取得(GAQL)

read-only。入札変更は不可

Meta Ads

pipeboard製(pipeboard-co/meta-ads-mcp)

広告管理全般

コミュニティ製。Meta公式ではない

TikTok Ads

AdsMCP製

キャンペーン管理・レポート

コミュニティ製

Yahoo Ads

専用MCPは未確認

--

筆者は自作で対応。Synter等のクロスプラットフォーム型でも代替可能

クロスプラットフォーム

Synter

Google, Meta, LinkedIn, Microsoft, Reddit, TikTok

複数媒体を一括管理

Google Ads MCP は Google 公式だがread-only(Google Developers)。レポートは引けるが入札を変えることはできない。Meta Ads MCP は pipeboard 社のオープンソース版が主流(GitHub)。Yahoo Ads は2026年4月時点で専用MCPサーバーの公開が確認できなかったため、筆者は自作した。

Routines で夜中も動くクラウド実行が可能に

2026年4月14日、Claude Code に「Routines」機能がリサーチプレビューとしてリリースされた(Anthropic公式ブログ)。

これまで、Claude Code をスケジュール実行するには自分のMacを起動しておく必要があった。Routines はクラウド上で動く。パソコンの電源を切っても、夜中でも、設定したスケジュール通りに実行される。

The Register は「少し賢い cron ジョブ」と表現した(The Register)。cronとは、Linuxの世界で昔からある「定時実行」の仕組みだ。それがAI付きでクラウドに乗った、と思えばいい。

トリガーは3種類。時間指定のスケジュール、APIからの呼び出し、GitHub上のイベント。Pro プランなら1日5回まで、Max プランなら1日15回まで実行できる(公式ドキュメント)。

ただし現時点ではリサーチプレビュー段階で、制限数は今後変わる可能性がある。

実際に作った監視システムの全体像

何をチェックしているか(20サーバー一覧)

筆者の環境では20のMCPサーバーを監視対象にしている。全部が広告関連ではない。業務で使っているツール全体を一括チェックする設計にした。

APIトークンの有効性を直接確認(12サーバー)

#

サーバー名

チェック方法

1

meta-ads

Graph API /me にトークンを投げて応答確認

2

instagram

同上(meta-ads と同じトークン)

3

n8n

REST API にAPIキーでリクエスト

4

brave-search

検索APIにサブスクリプショントークンでリクエスト

5

webflow

Bearer トークンで認証情報を取得

6

gemini

APIキーでモデル一覧を取得

7

clarity

JWTトークンの有効期限をデコードして確認

8

lark

App ID / App Secret でテナントトークンを発行

9

yahoo-ads

Python venv のモジュールインポート可否を確認

10

google-ads

credentials.json の refresh_token でトークンリフレッシュ

11

ahrefs

APIキーの設定有無 + パッケージの存在確認

12

gmail

ランチャースクリプトの存在確認

サーバー本体の存在確認(8サーバー)

#

サーバー名

チェック方法

13

chrome-devtools

npm パッケージの存在

14

context7

npm パッケージの存在

15

filesystem

npm パッケージの存在

16

memory-server

npm パッケージの存在

17

vibeframe

ローカルバイナリの存在

18

google-ads-server

Python スクリプトの存在

19

yahoo-ads-server

Python venv の存在

20

instagram-server

Python venv の存在

チェック方法はサーバーごとに違う。REST APIを叩けるものはHTTPリクエストで直接確認する。OAuth認証が複雑なもの(Yahoo Ads など)はバイナリやモジュールの存在で代替する。完璧ではないが、「起動できない」状態は検知できる。

失敗したら通知が飛ぶ仕組み

チェック結果は3段階で記録する。

  • PASS: 正常。トークンが有効、またはサーバーが利用可能
  • FAIL: 異常。トークン切れ、ファイル欠損、API応答エラー
  • SKIP: 設定が未完了。トークン未設定など(エラーではない)

FAILが1つでもあれば、macOSの通知センターにポップアップが出る。Discord Webhook を設定しておけば、スマホにも通知が飛ぶ。

Meta Token の自動更新ループ

60日で切れる Meta トークンへの対策は、別スクリプトで自動化した。

  • 毎週月曜の朝9時に debug_token で残り日数を確認
  • 残り14日を切っていたら fb_exchange_token で新しい60日トークンを取得
  • 設定ファイル(.mcp.json と secrets.env)を自動で書き換え
  • 書き換え後、新トークンで /me APIを叩いて検証
  • 結果をmacOS通知で報告

「14日前」に更新する理由は、バッファだ。スクリプトが何らかの理由で1〜2回失敗しても、まだ猶予がある。

ひとつ注意点がある。このスクリプトは既存の long-lived token を fb_exchange_token に渡してリフレッシュしている。Meta 公式ドキュメントでは short-lived token から long-lived token への交換としてのみ記載されており、この挙動は公式保証外だ。筆者環境では動作しているが、仕様変更で将来動かなくなる可能性はある。短期的には System User Token(無期限)の取得を検討する価値がある。

無期限の System User Token を使えばこの仕組み自体が不要になる。ただし「無期限」でも、パスワード変更やアプリ権限の取消で無効化されることがある(Meta公式ドキュメント)。どちらの方法を選んでも、定期的な debug_token チェックは入れておくのが安全だ。

コピペで動くコード(全公開)

ここから先はコードの全文を掲載する。経営者が自分で打ち込む必要はない。Claude Code に「このコードをコピーして、うちの環境に合わせてセットアップして」と頼めばいい。ただ、何をしているか理解しておくと、トラブル時に「何が壊れたか」の見当がつく。

MCP ヘルスチェックスクリプト

ファイルの場所: ~/Projects/mcp-health/mcp-health-check.sh

#!/bin/bash
# MCP API Health Check Script
# 全MCPサーバーのAPI疎通を検証し、結果をログ + macOS通知で報告
#
# 使い方:
#   ./mcp-health-check.sh          # 全チェック実行
#   ./mcp-health-check.sh --quiet  # 失敗時のみ通知

set -euo pipefail

# --- 基本設定 ---
# スクリプトが置いてあるフォルダを基準にする
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="$SCRIPT_DIR/logs"
mkdir -p "$LOG_DIR"

# ログファイル名に日時を入れる(あとから振り返れるように)
TIMESTAMP=$(date '+%Y-%m-%d_%H%M%S')
LOG_FILE="$LOG_DIR/health-check_${TIMESTAMP}.log"
LATEST_LOG="$LOG_DIR/latest.log"

# APIキーやトークンが書いてあるファイルを読み込む
# ※ このファイルは Git に入れない。.gitignore に追加すること
source ~/Projects/mcp-health/secrets.env

# Claude Code の設定ファイルからトークンを取り出す関数
# 第1引数: サーバー名(例: meta-ads)
# 第2引数: 環境変数名(例: META_ACCESS_TOKEN)
MCP_JSON="$HOME/.mcp.json"
get_mcp_env() {
  python3 -c "
import json, sys
with open('$MCP_JSON') as f:
    cfg = json.load(f)
server = cfg.get('mcpServers', {}).get('$1', {})
print(server.get('env', {}).get('$2', ''))
" 2>/dev/null
}

# --quiet オプション: 失敗したときだけ通知する(毎朝の自動実行向け)
QUIET_MODE=false
[[ "${1:-}" == "--quiet" ]] && QUIET_MODE=true

# Discord に通知を飛ばしたい場合はここに Webhook URL を入れる
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"

# --- カウンター(何件成功・何件失敗かを数える) ---
PASS=0
FAIL=0
SKIP=0
RESULTS=()

# ログを画面とファイル両方に出す関数
log() {
  echo "$1" | tee -a "$LOG_FILE"
}

# チェック結果を記録する関数
check_result() {
  local name="$1"    # サーバー名
  local status="$2"  # PASS / FAIL / SKIP
  local detail="${3:-}"  # 詳細メッセージ(任意)

  if [[ "$status" == "PASS" ]]; then
    PASS=$((PASS + 1))
    RESULTS+=("  PASS  $name")
  elif [[ "$status" == "SKIP" ]]; then
    SKIP=$((SKIP + 1))
    RESULTS+=("  SKIP  $name  $detail")
  else
    FAIL=$((FAIL + 1))
    RESULTS+=("  FAIL  $name  $detail")
  fi
}

# =============================================
# 個別チェック関数
# =============================================

# --- Meta広告: トークンでGraph APIの /me を叩く ---
check_meta_ads() {
  local token
  token=$(get_mcp_env "meta-ads" "META_ACCESS_TOKEN")
  if [[ -z "$token" ]]; then
    check_result "meta-ads" "SKIP" "token not configured"
    return
  fi
  local body
  body=$(curl -s --max-time 10 \
    "https://graph.facebook.com/v25.0/me?access_token=${token}" 2>/dev/null)
  if echo "$body" | python3 -c \
    "import sys,json; d=json.load(sys.stdin); sys.exit(0 if 'id' in d else 1)" \
    2>/dev/null; then
    check_result "meta-ads" "PASS"
  else
    local err
    err=$(echo "$body" | python3 -c \
      "import sys,json; print(json.load(sys.stdin).get('error',{}).get('message','unknown')[:80])" \
      2>/dev/null || echo "no response")
    check_result "meta-ads" "FAIL" "$err"
  fi
}

# --- Google Ads: credentials.jsonのrefresh_tokenでOAuthトークン更新 ---
check_google_ads() {
  local creds="$HOME/Projects/mcp-health/google-ads-credentials.json"
  if [[ ! -f "$creds" ]]; then
    check_result "google-ads" "FAIL" "credentials.json not found"
    return
  fi

  local refresh_token client_id client_secret
  refresh_token=$(python3 -c \
    "import json; print(json.load(open('$creds')).get('refresh_token',''))" 2>/dev/null)
  client_id=$(python3 -c \
    "import json; print(json.load(open('$creds')).get('client_id',''))" 2>/dev/null)
  client_secret=$(python3 -c \
    "import json; print(json.load(open('$creds')).get('client_secret',''))" 2>/dev/null)

  if [[ -z "$refresh_token" || -z "$client_id" || -z "$client_secret" ]]; then
    check_result "google-ads" "FAIL" "credentials.json missing fields"
    return
  fi

  local resp
  resp=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
    -X POST "https://oauth2.googleapis.com/token" \
    -d "client_id=${client_id}&client_secret=${client_secret}&refresh_token=${refresh_token}&grant_type=refresh_token" \
    2>/dev/null)
  if [[ "$resp" == "200" ]]; then
    check_result "google-ads" "PASS"
  else
    check_result "google-ads" "FAIL" "token refresh HTTP $resp"
  fi
}

# --- n8n: API キーでワークフロー一覧を1件取得 ---
check_n8n() {
  local resp
  resp=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
    -H "X-N8N-API-KEY: ${N8N_API_KEY}" \
    "https://your-instance.app.n8n.cloud/api/v1/workflows?limit=1" 2>/dev/null)
  if [[ "$resp" == "200" ]]; then
    check_result "n8n" "PASS"
  else
    check_result "n8n" "FAIL" "HTTP $resp"
  fi
}

# --- Brave Search: サブスクリプショントークンで検索テスト ---
check_brave_search() {
  local resp
  resp=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
    -H "X-Subscription-Token: ${BRAVE_API_KEY}" \
    "https://api.search.brave.com/res/v1/web/search?q=test&count=1" 2>/dev/null)
  if [[ "$resp" == "200" ]]; then
    check_result "brave-search" "PASS"
  else
    check_result "brave-search" "FAIL" "HTTP $resp"
  fi
}

# --- npmパッケージ系のMCPサーバー: パッケージが存在するか確認 ---
check_npm_package() {
  local name="$1"
  local pkg="$2"
  if npm list -g "$pkg" 2>/dev/null | grep -q "$pkg" || \
     ls ~/.npm/_npx/*/node_modules/"$pkg" 2>/dev/null | head -1 | grep -q .; then
    check_result "$name" "PASS" "(package cached)"
  else
    if npm view "$pkg" version --json 2>/dev/null | grep -q '"'; then
      check_result "$name" "PASS" "(package available on npm)"
    else
      check_result "$name" "FAIL" "package not found: $pkg"
    fi
  fi
}

# --- ローカルファイル系: バイナリやスクリプトが存在するか確認 ---
check_local_binary() {
  local name="$1"
  local path="$2"
  if [[ -f "$path" ]]; then
    check_result "$name" "PASS" "(binary exists)"
  else
    check_result "$name" "FAIL" "not found: $path"
  fi
}

# --- macOS通知 ---
send_macos_notification() {
  local title="$1"
  local message="$2"
  osascript -e "display notification \"$message\" with title \"$title\"" \
    2>/dev/null || true
}

# --- Discord通知(Webhook URL を設定している場合のみ) ---
send_discord_notification() {
  local message="$1"
  if [[ -n "$DISCORD_WEBHOOK_URL" ]]; then
    curl -s -H "Content-Type: application/json" \
      -d "{\"content\":\"$message\"}" \
      "$DISCORD_WEBHOOK_URL" >/dev/null 2>&1
  fi
}

# =============================================
# メイン実行
# =============================================

log "====================================="
log "MCP Health Check - $(date '+%Y-%m-%d %H:%M:%S')"
log "====================================="
log ""

# --- APIトークンチェック ---
log "[API Token Checks]"

check_meta_ads
check_google_ads
check_n8n
check_brave_search

# ここに自分の環境のチェック関数を追加する
# check_webflow
# check_gemini
# check_lark
# ...

# --- MCPサーバー本体の存在チェック ---
log ""
log "[MCP Server Binary Checks]"

check_npm_package "filesystem" "@modelcontextprotocol/server-filesystem"
check_npm_package "memory-server" "@modelcontextprotocol/server-memory"

# ここに自分の環境のサーバーを追加する
# check_local_binary "my-server" "$HOME/Projects/my-mcp/.venv/bin/python"

# --- 結果サマリー ---
TOTAL=$((PASS + FAIL + SKIP))

log ""
log "====================================="
log "Result: ${PASS}/${TOTAL} PASS | ${FAIL} FAIL | ${SKIP} SKIP"
log "====================================="
log ""

for r in "${RESULTS[@]}"; do
  log "$r"
done

# 最新ログへのリンクを更新
ln -sf "$LOG_FILE" "$LATEST_LOG"

# 30日より古いログを自動削除
find "$LOG_DIR" -name 'health-check_*.log' -mtime +30 -delete 2>/dev/null || true

# --- 通知の送信 ---
if [[ $FAIL -gt 0 ]]; then
  send_macos_notification "MCP Health Check" "${FAIL} servers FAILED. Check logs."
  if [[ -n "$DISCORD_WEBHOOK_URL" ]]; then
    send_discord_notification "**MCP Health Check** ${PASS}/${TOTAL} PASS | ${FAIL} FAIL"
  fi
  log ""
  log "Notification sent (${FAIL} failures detected)"
elif [[ "$QUIET_MODE" == false ]]; then
  send_macos_notification "MCP Health Check" "All ${PASS} servers OK"
  log "All checks passed"
fi

# 終了コード: FAILの件数を返す(0なら全成功)
exit $FAIL

ポイント解説(経営者向け)

  • secrets.env には APIキーやトークンを書く。Git には絶対に入れない
  • get_mcp_env は Claude Code の設定ファイル(.mcp.json)からトークンを読む。手動で2箇所管理しなくて済む
  • --quiet オプションをつけると、全部OKのときは何も通知しない。毎朝の自動実行で使う
  • 自分の環境にないサーバーのチェック関数はコメントアウトすればいい。最初は Meta と Google だけで十分

Meta Token 自動更新スクリプト

ファイルの場所: ~/Projects/mcp-health/meta-token-refresh.sh

#!/bin/bash
# Meta Access Token 自動更新スクリプト
#
# 動作:
# 1. 現在のトークンの残り有効期限を debug_token で確認
# 2. 残り14日以下なら、fb_exchange_token で新しい60日トークンに更新
# 3. 設定ファイル(.mcp.json と secrets.env)を自動で書き換え
# 4. 結果をmacOS通知で報告

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="$SCRIPT_DIR/logs"
mkdir -p "$LOG_DIR"

LOG_FILE="$LOG_DIR/meta-token-refresh_$(date '+%Y-%m-%d_%H%M%S').log"

# APIキー等を読み込む
source ~/Projects/mcp-health/secrets.env

log() {
  echo "[$(date '+%H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

send_notification() {
  osascript -e "display notification \"$2\" with title \"$1\"" 2>/dev/null || true
}

# 必要な環境変数が揃っているか確認
if [[ -z "${META_APP_ID:-}" || \
      -z "${META_APP_SECRET:-}" || \
      -z "${META_ACCESS_TOKEN:-}" ]]; then
  log "ERROR: META_APP_ID, META_APP_SECRET, META_ACCESS_TOKEN が secrets.env に必要"
  send_notification "Meta Token Refresh" "FAILED: missing env vars"
  exit 1
fi

# --- Step 1: 現在のトークンの有効期限を確認 ---
# debug_token はトークンの状態を返すMeta公式のAPI
# (参照: https://developers.facebook.com/docs/graph-api/reference/debug_token/)
log "Checking current token validity..."
DEBUG_RESP=$(curl -s \
  "https://graph.facebook.com/v25.0/debug_token?\
input_token=${META_ACCESS_TOKEN}&\
access_token=${META_APP_ID}|${META_APP_SECRET}")

# レスポンスから有効期限(UNIXタイムスタンプ)を取り出す
EXPIRES_AT=$(echo "$DEBUG_RESP" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)['data']
    print(d.get('expires_at', 0))
except: print(0)
")

# トークンが今も有効かどうか
IS_VALID=$(echo "$DEBUG_RESP" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)['data']
    print('1' if d.get('is_valid') else '0')
except: print(0)
")

NOW=$(date +%s)
DAYS_LEFT=$(( (EXPIRES_AT - NOW) / 86400 ))

log "Token valid: $IS_VALID, expires_at: $EXPIRES_AT ($DAYS_LEFT days left)"

# --- Step 2: 残り14日以下なら更新する ---
# 14日にしているのは、スクリプト自体が数回失敗しても猶予があるようにするため
if [[ "$IS_VALID" == "1" && $DAYS_LEFT -gt 14 ]]; then
  log "Token still has $DAYS_LEFT days. No refresh needed."
  exit 0
fi

if [[ "$IS_VALID" != "1" ]]; then
  log "WARNING: token is invalid. Attempting refresh anyway (may fail)..."
fi

# --- Step 3: fb_exchange_token で新しい60日トークンを取得 ---
# (参照: https://developers.facebook.com/docs/facebook-login/guides/access-tokens/get-long-lived/)
# 重要: client_secret を含むため、サーバーサイド(自分のPC)でのみ実行すること
#
# 注意: Meta公式ドキュメントでは fb_exchange_token は short-lived → long-lived の
# 交換用として説明されている。既存の long-lived token を渡してリフレッシュする手法は
# 実運用で動作することが多数報告されているが、公式に保証された挙動ではない。
# 仕様変更で将来動かなくなる可能性がある。
# 確実を期すなら Business Manager から System User Token(無期限)を発行し、
# このリフレッシュ処理自体を不要にする方法も検討する価値がある。
log "Refreshing token..."
REFRESH_RESP=$(curl -s -G \
  "https://graph.facebook.com/v25.0/oauth/access_token" \
  --data-urlencode "grant_type=fb_exchange_token" \
  --data-urlencode "client_id=${META_APP_ID}" \
  --data-urlencode "client_secret=${META_APP_SECRET}" \
  --data-urlencode "fb_exchange_token=${META_ACCESS_TOKEN}")

NEW_TOKEN=$(echo "$REFRESH_RESP" | python3 -c "
import sys, json
try: print(json.load(sys.stdin).get('access_token', ''))
except: print('')
")

if [[ -z "$NEW_TOKEN" ]]; then
  ERR=$(echo "$REFRESH_RESP" | python3 -c "
import sys, json
try: print(json.load(sys.stdin).get('error', {}).get('message', 'unknown')[:100])
except: print('parse error')
")
  log "ERROR: token refresh failed: $ERR"
  send_notification "Meta Token Refresh" "FAILED: $ERR"
  exit 1
fi

log "New token obtained (length: ${#NEW_TOKEN})"

# --- Step 4: 設定ファイルを自動で書き換え ---
# .mcp.json: Claude Code がMCPサーバーに渡すトークンの設定
python3 <<EOF
import json
path = '$HOME/.mcp.json'
with open(path) as f:
    cfg = json.load(f)
# meta-ads と instagram の両方を更新
cfg['mcpServers']['meta-ads']['env']['META_ACCESS_TOKEN'] = "${NEW_TOKEN}"
cfg['mcpServers']['instagram']['env']['META_ACCESS_TOKEN'] = "${NEW_TOKEN}"
with open(path, 'w') as f:
    json.dump(cfg, f, indent=2, ensure_ascii=False)
EOF
log "Updated .mcp.json"

# secrets.env: ヘルスチェックスクリプトが参照するトークン
SECRETS_FILE="$HOME/Projects/mcp-health/secrets.env"
sed -i '' "s|^META_ACCESS_TOKEN=.*|META_ACCESS_TOKEN=${NEW_TOKEN}|" "$SECRETS_FILE"
log "Updated secrets.env"

# --- Step 5: 新トークンで本当に動くか検証 ---
VERIFY_RESP=$(curl -s \
  "https://graph.facebook.com/v25.0/me?access_token=${NEW_TOKEN}")
if echo "$VERIFY_RESP" | grep -q '"id"'; then
  NAME=$(echo "$VERIFY_RESP" | python3 -c \
    "import sys,json; print(json.load(sys.stdin).get('name','?'))")
  log "Verification OK: user=$NAME"
  send_notification "Meta Token Refresh" "OK: refreshed for $NAME (60 days)"
else
  log "ERROR: new token verification failed"
  send_notification "Meta Token Refresh" "FAILED: new token invalid"
  exit 1
fi

log "Done."

ポイント解説(経営者向け)

  • debug_tokenfb_exchange_token は Meta の公式API。怪しいツールではない
  • client_secret が含まれるため、このスクリプトは自分の Mac 上でだけ動かす。Webサーバーに置かない
  • 更新に成功すると .mcp.jsonsecrets.env の2ファイルが自動で書き換わる。次回の Claude Code セッションから新トークンが使われる
  • 失敗するとmacOS通知が飛ぶ。スマホ通知が欲しければ Discord Webhook を追加する

launchd で毎朝9時に動かす

macOS には launchd という「指定した時間にスクリプトを動かす」仕組みがある。Windowsの「タスクスケジューラ」に相当する。cron(Linux の定時実行)と違い、Mac がスリープしていて実行を逃した場合、復帰後にまとめて1回実行してくれる(launchd.info)。朝 Mac を開いたら自動で走る。

ファイル1: ヘルスチェック用(毎日9:00)

場所: ~/Library/LaunchAgents/com.yourdomain.mcp-healthcheck.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- この設定の名前。他と被らなければ何でもいい -->
    <key>Label</key>
    <string>com.yourdomain.mcp-healthcheck</string>

    <!-- 実行するコマンド -->
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/yourname/Projects/mcp-health/mcp-health-check.sh</string>
        <string>--quiet</string>
    </array>

    <!-- 毎日 09:00 に実行 -->
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>

    <!-- ログの出力先 -->
    <key>StandardOutPath</key>
    <string>/Users/yourname/Projects/mcp-health/logs/launchd-stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/yourname/Projects/mcp-health/logs/launchd-stderr.log</string>

    <!-- PATHを通す(Homebrew等を使えるように) -->
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
        <key>HOME</key>
        <string>/Users/yourname</string>
    </dict>
</dict>
</plist>

ファイル2: Meta トークン更新用(毎週月曜9:00)

場所: ~/Library/LaunchAgents/com.yourdomain.meta-token-refresh.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.yourdomain.meta-token-refresh</string>

    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/yourname/Projects/mcp-health/meta-token-refresh.sh</string>
    </array>

    <!-- 毎週月曜 09:00 に実行(Weekday 1 = 月曜日) -->
    <key>StartCalendarInterval</key>
    <dict>
        <key>Weekday</key>
        <integer>1</integer>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>

    <key>StandardOutPath</key>
    <string>/Users/yourname/Projects/mcp-health/logs/meta-refresh-stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/yourname/Projects/mcp-health/logs/meta-refresh-stderr.log</string>

    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
        <key>HOME</key>
        <string>/Users/yourname</string>
    </dict>
</dict>
</plist>

2ファイルとも保存したら、ターミナルで以下を実行する。

# launchd に登録
# 注: launchctl load は非推奨だが現在も動作する。
# 新しい書式: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/xxx.plist
launchctl load ~/Library/LaunchAgents/com.yourdomain.mcp-healthcheck.plist
launchctl load ~/Library/LaunchAgents/com.yourdomain.meta-token-refresh.plist

# 登録確認
launchctl list | grep yourdomain

/Users/yourname/ の部分は自分のユーザー名に置き換える。Claude Code に「このplistを自分の環境に合わせて」と言えば書き換えてくれる。

Routines(クラウド実行)との使い分け

項目

launchd(ローカル)

Claude Code Routines(クラウド)

PCの電源

ONである必要あり(スリープは復帰後に実行)

不要。クラウドで動く

ローカルファイル参照

可能

不可(fresh clone)

最小間隔

1分

1時間

料金

無料(macOS標準機能)

Claude Pro $20/月に含まれる(日5回まで)

おすすめ用途

secrets.env の読み書き、ローカルMCPの検証

レポート生成、GitHub連携、リモート監視

筆者の場合、ヘルスチェックは launchd で毎朝ローカル実行し、週次レポートの生成は Routines に任せる、という使い分けをしている。

実行ログのサンプル

実際の実行結果はこう出る。

=====================================
MCP Health Check - 2026-04-15 13:55:20
=====================================

[API Token Checks]

[MCP Server Binary Checks]

=====================================
Result: 20/20 PASS | 0 FAIL | 0 SKIP
=====================================

  PASS  meta-ads
  PASS  instagram
  PASS  n8n
  PASS  brave-search
  PASS  webflow
  PASS  gemini
  PASS  clarity
  PASS  lark
  PASS  yahoo-ads
  PASS  google-ads
  PASS  ahrefs
  PASS  gmail
  PASS  chrome-devtools
  PASS  context7
  PASS  filesystem
  PASS  memory-server
  PASS  vibeframe
  PASS  google-ads-server
  PASS  yahoo-ads-server
  PASS  instagram-server
All checks passed

20/20 PASS。全サーバー正常。これが毎朝9時に自動で走り、FAILがあればスマホに通知が来る。

Meta トークンの自動更新ログはこうなる(更新不要の場合)。

[13:55:05] Checking current token validity...
[13:55:05] Token valid: 1, expires_at: 1781412337 (59 days left)
[13:55:05] Token still has 59 days. No refresh needed.

残り59日。14日を切ったら自動で新しいトークンに切り替わる。

経営者のための ROI 計算

データ取得が止まると何が起きるか

冒頭で書いた通り、トークンが切れても広告配信は止まらない。止まるのは「データの取得」だ。自動レポートが壊れ、入札の自動調整が停止し、異常検知も機能しなくなる。つまり、広告は回り続けるが、最適化が効かない状態で垂れ流しになる

ここからはモデル試算。前提を変えれば数字も変わるので、あくまで「考え方のフレームワーク」として自社の数字を当てはめてほしい。

前提条件(中小EC・月100万円の広告費)

項目

数値

根拠

月間広告費

100万円

中小EC想定

日あたり広告費

約3.3万円

100万円 / 30日

通常時CPA

5,000円

広告費3.3万円で約6.6件/日の獲得を想定

最適化停止時のCPA悪化

+25%(6,250円)

入札調整・除外設定が12時間止まった場合の想定

12時間の追加コスト

約1,000円

(6,250 - 5,000) x 約1.65万円分の配信 / 6,250

計算式: 日あたり広告費の半分(12時間分) x CPA悪化率 = 無駄になった広告費

もう少し具体的に書くと、CPA 5,000円が6,250円に悪化した状態で12時間回り続けると、同じ獲得数を得るのに約1,000円余計にかかる。これは控えめな見積もりだ。成果の悪いキーワードやオーディエンスへの配信が止められない場合、CPA悪化が50%を超えることもある。

筆者の経験では、Meta のトークン更新を手動管理していた時期は年に2〜3回のトラブルがあった。そのたびに「データ欠損を埋める手作業」と「異常に気づくまで放置された広告費のロス」が発生していた。

年間の損失イメージ(算出式):

  • 年1回の場合: CPA悪化分 約1,000円 + 手動復旧の工数 → 実害は軽微だが「気づかなかった時間」の不安が残る
  • 年3回の場合: CPA悪化分 約3,000円 + レポート欠損の手動補完(1回あたり1〜2時間) → 工数コストが本丸
  • 月1回の場合: CPA悪化分 約12,000円/年 + 入札判断の遅延による機会損失 → 自動化の投資対効果が明確になる

(この試算はモデルケース。CPA悪化率は広告の種類、自動入札の依存度、時間帯によって大きく変わる。自社の数字を当てはめるには、上の式の「通常CPA」「悪化率」「放置時間」を差し替える)

構築コストと運用コスト

項目

コスト

Claude Code Pro プラン

$20/月(約2,600円)

初期構築の工数

約3時間(Claude Code に指示を出しながら)

月次のメンテナンス

ほぼゼロ(自動実行、ログ確認は月1回程度)

年間コスト

約3.2万円($20 x 12ヶ月)

年間3.2万円。トークン失効の自動検知・自動更新だけでなく、20サーバーの死活監視が毎朝走る。金額よりも「気づけなかった」というリスクをゼロにできることが本質的な価値だ。

月1回ペースでAPIまわりのトラブルが起きる環境なら、CPA悪化分+手動復旧の工数を合算して年間数万〜十数万円のコスト削減が見込める。年間3.2万円の投資で「夜中の異常を翌朝9時に検知する仕組み」が手に入ると考えれば、費用対効果は明確だ。

外部ツール(Supermetrics / Improvado)との比較

ツール

月額

年額

主な用途

Claude Code + MCP

$20(約2,600円)

約3.2万円

監視、トークン管理、レポート生成

Supermetrics Starter

$37(約4,800円、年間契約のみ)

約5.8万円

データ集約(Google Sheets / Looker Studio 連携)

Improvado

$3,000〜$10,000(レビューサイト調べ)

360万円〜1,200万円

エンタープライズ向け全自動ETL

Supermetrics は広告データの集約が本業で、APIトークンの監視が主目的ではない。Improvado は大企業向けだ。

Claude Code + MCP の利点は「監視ロジックを自分で書ける」こと。何をチェックし、どう通知し、何を自動修復するか、全部カスタマイズできる。欠点は、最低限のターミナル操作は必要なこと。とはいえ、Claude Code 自身に「セットアップして」と頼めるので、ハードルは思ったより低い。

(Supermetrics の価格は公式サイトから、Improvado の価格帯はG2等の中立レビューサイトから推定。Improvado は公式に価格を公開しておらず、実際の見積もりは問い合わせが必要。比較対象は機能カテゴリが異なり、重なる部分は限定的)

外注するか自社で Claude Code を使うか

観点

代理店に外注

自社で Claude Code

月額コスト

広告費の20%前後が相場

$20 + 自分の時間

トークン管理

代理店任せ(手動の場合あり)

自動化済み

異常の検知速度

営業時間内(代理店の稼働時間)

24時間(自動通知)

カスタマイズ

要相談・追加費用

自分で即変更可能

専門知識

不要(代理店が持つ)

最低限のターミナル操作

代理店に任せているなら、この仕組みは「代理店の仕事を検証するツール」として使える。広告が止まっていないか、トークンが有効か、自分側でも監視する。代理店への信頼が不安ではなく、単なるダブルチェックだ。

導入の最初の一歩

Claude Code Pro($20/月)を契約する

claude.com/pricing から Pro プランを契約する。Free プランでは Claude Code が使えない。

Pro で使えること:

  • ターミナル・VS Code拡張・デスクトップアプリ、どこからでも Claude Code を使える
  • Routines で日5回までスケジュール実行
  • MCP サーバーの接続数に上限なし

Max プラン($100〜$200/月)は、利用量の上限が5〜20倍になる。最初は Pro で十分。足りなくなったら上げればいい。

MCP を1つだけ入れてみる

最初から20サーバーを設定する必要はない。まずは1つ。

おすすめの最初の1つは filesystem(ファイルシステム)。ローカルのファイルを読み書きできるMCPサーバーで、APIキーも不要。これで「MCPとはどういう仕組みか」を体感できる。

# Claude Code を起動して
claude

# MCP サーバーを追加(対話形式で案内される)
/mcp add filesystem

その次に、自分が使っている広告媒体のMCPを追加する。Google Ads なら claude mcp add google-ads、Meta なら pipeboard の meta-ads-mcp を設定する。

Claude Code での MCP 設定方法は公式ドキュメントに詳しい。

失敗してもやり直せる設計で始める

この仕組みの良いところは、壊れても広告自体は止まらないこと。

ヘルスチェックスクリプトはread-only。広告の設定を変えたり、入札を操作したりはしない。壊れたら「通知が来なくなる」だけで、広告配信には影響しない。

Meta トークン更新スクリプトも、更新に失敗したら通知を出して終わる。無理やり書き換えたりしない。

だから「とりあえず動かしてみる」で問題ない。完璧を目指して準備に時間をかけるより、まず動かして、必要なものを足していく方が結果的に早い。

3つだけ覚えてほしい

2025年、日本のインターネット広告費は4兆459億円に達し、広告費全体の50%を超えた(電通「2025年 日本の広告費」)。広告がデジタルに集中するほど、APIの健全性は経営の問題になる。

Claude Code + MCP の組み合わせは、月$20から始められる。8,000超のMCPサーバーが公式レジストリに登録されていて、Google Ads、Meta Ads、TikTok Ads と直接つながる。2026年4月にリサーチプレビューとして公開された Routines で、PCを閉じても夜間監視が動くようになった。

構築は筆者の場合で3時間。年間コストは約3.2万円。広告のトークン切れを年1回防ぐだけで元が取れる。

最初のステップは Pro プランの契約と、MCP サーバー1つの追加。それだけでいい。

関連記事