0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

claude -pとblastengine MCPで週次の配信レポートを自動生成する

0
Posted at

blastengine MCPサーバーは、対話しながら使うと便利です。ただ、毎週月曜の朝に配信状況を確認する、といった定型作業まで人が対話で回すのはもったいない話です。

Claude Codeには非対話モード(claude -p)があり、MCPサーバーもそのまま使えます。本記事では、blastengine MCPの参照系ツールだけを許可した状態で週次の配信レポートを生成し、Slackに流すところまでを組みます。メールは1通も送りません。送らない構成にすることが、そのまま安全装置になります。

blastengine MCPの導入は導入記事、ツールの一覧は全15ツールの検証記事を参照してください。

設計方針:三重に封じ込める

非対話モードでは、実行の途中で人が「許可しますか」と聞かれることがありません。だからこそ、何ができる状態で起動するかを起動時に決め切る必要があります。今回は3つの層で絞ります。

手段 効果
blastengine側 送信系フラグを立てない 送信ツールを呼んでも実行時に弾かれる
MCP設定 --mcp-config--strict-mcp-config このジョブ専用のサーバーだけを読み込む
ツール許可 --allowedTools--tools "" 呼べるツールを参照系だけに固定する

どれか1つでも成立していれば送信は起きませんが、3つとも設定しておけば、設定ファイルを1つ間違えたくらいでは事故になりません。

準備1:レポート専用のMCP設定を作る

普段使いの設定とは分けて、このジョブ専用のJSONを作ります。送信系フラグはすべてfalseです。

{
  "mcpServers": {
    "blastengine": {
      "type": "stdio",
      "command": "node",
      "args": ["/absolute/path/to/blastengine-mcp/dist/index.js"],
      "env": {
        "BLASTENGINE_LOGIN_ID": "${BLASTENGINE_LOGIN_ID}",
        "BLASTENGINE_API_KEY": "${BLASTENGINE_API_KEY}",
        "BLASTENGINE_ENABLE_SEND": "false",
        "BLASTENGINE_ENABLE_BULK": "false",
        "BLASTENGINE_ENABLE_CSV_IMPORT": "false"
      }
    }
  }
}

${VAR}はシェルの環境変数から展開されます。認証情報をファイルに直書きしないための書き方で、--mcp-configで読み込む場合も同じように効きます。このファイルを~/jobs/blastengine-report/mcp.jsonとして置いたとします。

起動時に--strict-mcp-configを付けると、この設定以外のMCPサーバー(ユーザースコープやプロジェクトスコープに登録済みのもの)は読み込まれません。普段の環境に何が繋がっていようと、このジョブにはblastengineだけが見える状態になります。

準備2:許可するツールを列挙する

MCPツールの名前はmcp__<サーバー名>__<ツール名>の形式です。上の設定ではサーバー名をblastengineにしたので、たとえば使用量取得はmcp__blastengine__blastengine_usage_latest_getになります。

レポートに必要なのは次の4つです。

  • mcp__blastengine__blastengine_usage_latest_get … 最新の使用量
  • mcp__blastengine__blastengine_usage_month_get … 指定月の使用量
  • mcp__blastengine__blastengine_deliveries_list … 配信の検索
  • mcp__blastengine__blastengine_mail_results_list … 宛先ごとの配信ログ

--allowedToolsにはこの4つだけを並べます。ワイルドカードに頼らず、1つずつ書くほうが意図がはっきりします。あわせて--tools ""を指定すると、BashやWriteといった組み込みツールもすべて無効になります。レポートの出力は標準出力をシェル側でリダイレクトすれば済むので、ファイル書き込みの権限は要りません。

準備3:日時を先に解決しておく

blastengineの検索ツールに渡す日時は、タイムゾーンオフセット付きのISO 8601が必須です。「先週」のような相対表現は受け付けません。

これは非対話運用ではむしろ好都合で、期間をシェル側で確定させてからプロンプトに埋め込めば、実行日によって解釈がぶれることがなくなります。

export TZ=Asia/Tokyo
TODAY=$(date '+%Y-%m-%d')
SINCE=$(date -v-7d '+%Y-%m-%d' 2>/dev/null || date -d '7 days ago' '+%Y-%m-%d')
START="${SINCE}T00:00:00+09:00"
END="${TODAY}T00:00:00+09:00"

date -vはmacOS(BSD)、date -dはGNU coreutilsです。上の書き方ならどちらでも動きます。

準備4:出力の形を決める

--json-schemaを使うと、最終出力をJSONスキーマに沿った形に固定できます。後段でSlackに流したり、CSVに追記したりするなら、テキストのまま受け取るより扱いが楽です。

{
  "type": "object",
  "properties": {
    "period_start": { "type": "string" },
    "period_end": { "type": "string" },
    "delivery_count": { "type": "integer" },
    "hard_error_count": { "type": "integer" },
    "soft_error_count": { "type": "integer" },
    "usage_note": { "type": "string" },
    "highlights": { "type": "array", "items": { "type": "string" } },
    "summary": { "type": "string" }
  },
  "required": ["period_start", "period_end", "summary"]
}

--output-format jsonと組み合わせると、レスポンスのresultにJSON文字列が、structured_outputにパース済みのオブジェクトが入ります。後段ではstructured_outputを読むほうが確実です。

実行スクリプト

ここまでをまとめます。

#!/usr/bin/env bash
set -euo pipefail

export TZ=Asia/Tokyo
# cron の PATH は最小限なので、MCPサーバーが要求する Node.js(22.15以上)が
# 含まれる場所を明示的に足しておく
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin"

JOB_DIR="$HOME/jobs/blastengine-report"
CLAUDE_BIN="/absolute/path/to/claude"   # command -v claude の結果を書く
LOCK_DIR="$JOB_DIR/.lock"

# 二重起動を防ぐ(mkdir はアトミック)
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
  echo "already running" >&2
  exit 0
fi
trap 'rmdir "$LOCK_DIR"' EXIT

# 認証情報(ファイルの権限は 600 にしておく)
source "$JOB_DIR/.env"

TODAY=$(date '+%Y-%m-%d')
SINCE=$(date -v-7d '+%Y-%m-%d' 2>/dev/null || date -d '7 days ago' '+%Y-%m-%d')
START="${SINCE}T00:00:00+09:00"
END="${TODAY}T00:00:00+09:00"
MONTH=$(date '+%Y%m')

PROMPT=$(cat <<PROMPT_EOF
blastengineの配信状況を集計してください。対象期間は ${START} 以上 ${END} 未満です。

1. blastengine_deliveries_list で、この期間の配信を検索する(delivery_type は TRANSACTION と BULK)
2. blastengine_mail_results_list で、この期間の HARDERROR と SOFTERROR の件数を確認する
3. blastengine_usage_month_get で ${MONTH} の使用量を確認する

集計結果をもとに、運用担当が朝に読む前提の短いレポートを作ってください。
件数が取得上限に達していて全体を数え切れない場合は、その旨を usage_note に明記してください。
推測で数字を埋めないでください。
PROMPT_EOF
)

OUT=$("$CLAUDE_BIN" -p "$PROMPT" \
  --mcp-config "$JOB_DIR/mcp.json" \
  --strict-mcp-config \
  --tools "" \
  --allowedTools \
    mcp__blastengine__blastengine_usage_latest_get \
    mcp__blastengine__blastengine_usage_month_get \
    mcp__blastengine__blastengine_deliveries_list \
    mcp__blastengine__blastengine_mail_results_list \
  --json-schema "$(cat "$JOB_DIR/schema.json")" \
  --output-format json \
  --max-budget-usd 1.0 < /dev/null)

echo "$OUT" > "$JOB_DIR/last-run.json"

# 失敗検知
IS_ERROR=$(echo "$OUT" | python3 -c 'import sys,json; print("1" if json.load(sys.stdin).get("is_error") else "0")')
DENIALS=$(echo "$OUT" | python3 -c 'import sys,json; print(len(json.load(sys.stdin).get("permission_denials", [])))')

if [ "$IS_ERROR" = "1" ] || [ "$DENIALS" != "0" ]; then
  echo "run failed (is_error=$IS_ERROR denials=$DENIALS)" >&2
  exit 1
fi

# Slackへ通知
SUMMARY=$(echo "$OUT" | python3 -c '
import sys, json
d = json.load(sys.stdin)["structured_output"]
head = "blastengine 週次レポート({} 〜 {})".format(d["period_start"], d["period_end"])
lines = [head, d["summary"]]
lines += ["・" + h for h in d.get("highlights", [])]
if d.get("usage_note"):
    lines.append("※" + d["usage_note"])
print("\n".join(lines))
')

curl -sS -X POST -H 'Content-type: application/json' \
  --data "$(python3 -c 'import json,sys; print(json.dumps({"text": sys.stdin.read()}))' <<< "$SUMMARY")" \
  "$SLACK_WEBHOOK_URL" > /dev/null

実際に回すと、レスポンスはこうなります。参照系4ツールに絞った週次レポートで、5ターン・0.11ドル(Sonnet指定)でした。--modelを省くとセッションの既定モデルが使われるので、コストを読みたいなら明示しておくと安定します。

{
  "is_error": false,
  "num_turns": 5,
  "permission_denials": [],
  "total_cost_usd": 0.1103,
  "structured_output": {
    "period_start": "2026-08-19T00:00:00+09:00",
    "period_end": "2026-08-26T23:59:59+09:00",
    "delivery_count": 2,
    "hard_error_count": 0,
    "soft_error_count": 0,
    "summary": "対象期間の配信は2件(TRANSACTION 1件、BULK 1件)、いずれもSENT。..."
  }
}

スクリプトの細部について、実際に回して分かったことを2つ補足します。

claude -pは標準入力を待つので、リダイレクトを付けないと3秒待ってから警告を出して進みます。cronから起動する分には実害はありませんが、< /dev/nullを付けておくのが確実です。

もう1つがPATHです。cronの環境変数は最小限なので、nodeがどこに解決されるかは環境次第になります。筆者の環境では/usr/local/bin/node(v18)が拾われました。MCPサーバーはNode.js 22.15以上を要求しているので、意図したバージョンが入っているディレクトリを明示的に足すか、mcp.jsoncommandを絶対パスで書いてください。

permission_denialsを見ているのが地味に効きます。許可リストを絞りすぎてツールを呼べなかった場合、Claudeは「取得できませんでした」と書いたレポートを平然と返してくることがあります。実際に許可リストから外して動かすと、こうなりました。

run failed (is_error=0 denials=4)
exit=1

注目したいのはis_error0、つまり実行自体は成功扱いになっている点です。ツールを1つも呼べていないのに、レポートは生成され、is_errorだけ見ていれば成功として通ってしまいます。空のレポートが毎週届き続ける事態を避けるため、拒否が1件でもあれば失敗として扱います。

定期実行に載せる

cronなら次のようになります。

0 9 * * 1 /usr/bin/env bash /Users/you/jobs/blastengine-report/run.sh >> /Users/you/jobs/blastengine-report/run.log 2>&1

macOSで常用するならlaunchdのほうが素直です。どちらの場合も、対話でログインしたシェルとは環境が違う点に注意してください。最初は同じユーザーで手動実行し、認証が通ること、PATHが足りていることを確かめてから登録します。

ログは残しておきます。--output-format jsonのレスポンスにはsession_idが含まれるので、あとからclaude --resume <session_id>でその実行の中身を追えます。

コストの見え方

--output-format jsonのレスポンスにはtotal_cost_usdusageが入っています。週次で回すなら、この値をログに蓄積しておくと、月あたりのコストがそのまま見えます。

--max-budget-usdを指定しておけば、想定外にループしたときの上限を切れます。参照系だけのレポートで1ドルを超えることはまずないので、上限は低めに置いて構いません。上限に当たったら失敗として検知されます。

つまずきやすいポイント

症状 原因と対処
ツールが呼ばれず空のレポートが届く --allowedToolsの綴り違い。mcp__<サーバー名>__<ツール名>の形式で、設定ファイル側のサーバー名と一致しているか確認する
missing_credentialsになる cron環境に環境変数が渡っていない。スクリプト内で明示的に読み込む
日時の指定を弾かれる オフセット付きISO 8601が必須。2026-08-24のような日付のみは不可
件数が実態と合わない blastengine_mail_results_listは1回あたり最大1000件。超える場合はanchorでページングが必要で、AIが取り切れているとは限らない。合計値は配信単位の情報と突き合わせる
使用量が送信件数に追いつかない usage_month_get / usage_latest_getの集計には反映のラグがある。updated_timeを見て、直近の配信が含まれているかを判断する
先月分のレポートが空になる 配信情報とログは配信開始から62日で削除される。長期の推移を持ちたいなら、この期間内に自分側へ保存する
2つのジョブが重なって動く 前回の実行が終わっていない。ロックを取る(本文のスクリプト参照)
いつの間にか送信できる状態になっている 普段使いのMCP設定を読み込んでいる。--strict-mcp-configを付ける

応用:バウンスの監視に寄せる

同じ枠組みで、レポートではなく監視に振ることもできます。プロンプトを「直近24時間でHARDERRORが閾値を超えていたら、その配信IDと宛先ドメインの内訳を返す」に変え、閾値以下なら通知しない条件を付ければ、静かな日は何も鳴らない監視になります。

このとき重要なのは、通知の判断をシェル側の条件分岐に持たせることです。AIの出力に「異常はありません」と書かせて人が読むより、structured_outputの数値を見てシェルが判定するほうが、見落としが起きません。

まとめ

claude -pとblastengine MCPを組み合わせた非対話運用のポイントは3つです。

  • 参照系だけに絞る。blastengine側の送信系フラグを立てず、--allowedToolsで呼べるツールを列挙し、--strict-mcp-config--tools ""で余計なものを混ぜない
  • 日時はシェルで確定させてからプロンプトに埋め込む。オフセット付きISO 8601が必須という制約は、非対話ではむしろ再現性の担保になる
  • --json-schemaで出力を固定し、is_errorpermission_denialsで失敗を検知する。判断はAIの文章ではなく数値でシェルが行う

対話で使うMCPと、バッチで使うMCPは、同じサーバーでも設計が変わります。送らない構成を先に作っておくと、送る構成に踏み出すときの安心材料になります。


blastengine MCPサーバーへの要望や感想は、以下のアンケートフォームから送ると開発チームに届くみたいです。

https://docs.google.com/forms/d/e/1FAIpQLSdNZ9TswUT3JEv3pMHkJGqOvgmsXuKP1smkJiNEsqbTVDwRyg/viewform

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?