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.jsonのcommandを絶対パスで書いてください。
permission_denialsを見ているのが地味に効きます。許可リストを絞りすぎてツールを呼べなかった場合、Claudeは「取得できませんでした」と書いたレポートを平然と返してくることがあります。実際に許可リストから外して動かすと、こうなりました。
run failed (is_error=0 denials=4)
exit=1
注目したいのはis_errorが0、つまり実行自体は成功扱いになっている点です。ツールを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_usdとusageが入っています。週次で回すなら、この値をログに蓄積しておくと、月あたりのコストがそのまま見えます。
--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_errorとpermission_denialsで失敗を検知する。判断はAIの文章ではなく数値でシェルが行う
対話で使うMCPと、バッチで使うMCPは、同じサーバーでも設計が変わります。送らない構成を先に作っておくと、送る構成に踏み出すときの安心材料になります。
blastengine MCPサーバーへの要望や感想は、以下のアンケートフォームから送ると開発チームに届くみたいです。
https://docs.google.com/forms/d/e/1FAIpQLSdNZ9TswUT3JEv3pMHkJGqOvgmsXuKP1smkJiNEsqbTVDwRyg/viewform