顧客への案内メールの宛先を、スプレッドシートで管理しているチームは多いと思います。会社名や担当者名、契約プランを1行ずつ並べたシートがあれば、あとは「1行ずつ差し込んで送り、送った結果を同じ行に残す」ができれば足ります。
Googleスプレッドシートを読み書きするMCPサーバー(xing5/mcp-google-sheets)と、blastengineのMCPサーバーを1つのエージェントにつなぐと、この流れを会話で回せます。本記事ではClaude Codeでこの構成を組み、シートの行を抽出して差し込みメールを送り、配信結果をシートに書き戻すところまで確認します。
構成自体はkintoneの記事と同じ形です。今回はスプレッドシートならではのつまずき(MCPサーバーの起動、サービスアカウントの扱い、セルの値の型、ツール定義のコンテキスト量)を中心に書きます。
※記事中の送信者名と送信元メールアドレスは、掲載用に置き換えています。
blastengine MCPの導入手順は導入記事、各ツールの入出力は全15ツールの検証記事を参照してください。
全体構成
┌──────────────────────────────┐
│ Claude Code │
└───────┬──────────────┬───────┘
stdio │ │ stdio
(uvx) │ │
┌────────────────▼───┐ ┌───▼──────────────────┐
│ mcp-google-sheets │ │ blastengine MCP │
│ (Python) │ │ │
└────────┬───────────┘ └───────────┬──────────┘
│ Sheets API / Drive API │
│ (サービスアカウント) │
┌────────▼───────────┐ ┌───────────▼──────────┐
│ Googleスプレッド │ │ blastengine │
│ シート │ │ │
└────────────────────┘ └──────────────────────┘
どちらも手元で動くstdioサーバーです。mcp-google-sheetsはPython製で、uvxで起動します。Googleへの認証はサービスアカウントで行います。
Googleスプレッドシート用のMCPサーバーはほかにもいくつか公開されています。今回はスター数が多く、サービスアカウント認証とツールの絞り込みに対応しているxing5/mcp-google-sheetsを選びました。
準備1:サービスアカウントを作り、シートを共有する
Google Cloudのコンソールで次の作業をします。
- プロジェクトを選び(なければ作り)、「APIとサービス」からGoogle Sheets APIとGoogle Drive APIを有効にする
- 「IAMと管理」→「サービスアカウント」でサービスアカウントを作る。ロールは付けなくてかまわない
- 作ったサービスアカウントの「鍵」タブで、JSON形式の鍵を作ってダウンロードする
鍵ファイルは、パスワードと同じ扱いです。リポジトリに含めず、chmod 600などで自分だけが読めるようにしておきます。組織のポリシーでサービスアカウントの鍵の作成が禁止されている環境では、手順3で止まります。その場合は管理者に相談してください。
シートは人が作って、サービスアカウントに共有する
サービスアカウントは、自分ではスプレッドシートを作れません。mcp-google-sheetsにはcreate_spreadsheetというツールがあります。ところが、サービスアカウントの認証で呼ぶと、次のエラーで失敗しました。
403 storageQuotaExceeded
The user's Drive storage quota has been exceeded.
サービスアカウントにはファイルを置くためのドライブの容量がないため、ファイルを所有できません。そこで、シートは自分のGoogleドライブで作り、サービスアカウントに共有します。
- Googleドライブで空のスプレッドシートを作る
- 右上の「共有」から、サービスアカウントのメールアドレス(
(名前)@(プロジェクトID).iam.gserviceaccount.com)を編集者として追加する - URLの
/d/と/editの間にある文字列(スプレッドシートID)を控えておく
サービスアカウントから見えるのは、共有されたファイルだけです。エージェントにドライブ全体を渡さずに済むので、扱うシートを限定したい用途にはかえって都合がいい形です。
準備2:Claude Codeに2つのMCPを登録する
プロジェクト直下の.mcp.jsonに2つのサーバーを書きます。
{
"mcpServers": {
"sheets": {
"type": "stdio",
"command": "uvx",
"args": ["--with", "mcp<2", "mcp-google-sheets@0.6.3"],
"env": {
"SERVICE_ACCOUNT_PATH": "/absolute/path/to/service-account.json",
"ENABLED_TOOLS": "list_sheets,get_sheet_data,update_cells,batch_update_cells"
}
},
"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": "true",
"BLASTENGINE_ENABLE_BULK": "false",
"BLASTENGINE_ENABLE_CSV_IMPORT": "false"
}
}
}
}
sheets側の設定には2点、READMEの記載と違うところがあります。
uvx mcp-google-sheetsのままでは起動しない
READMEにはuvx mcp-google-sheets@latestで起動する例が載っています。手元でこのとおりに起動すると、次のエラーで落ちました。
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
mcp-google-sheetsは、MCPのPython SDK(mcpパッケージ)に依存しています。依存のバージョンに上限がないため、uvxで起動すると最新の2.x系が入ります。2.x系ではFastMCPがMCPServerに改名されていて、mcp.server.fastmcpをimportする0.6.3のコードが動きません。
--with "mcp<2"を付けて1.x系に固定すると起動しました(手元ではmcp 1.28.1が入りました)。あわせてサーバー本体のバージョンも@0.6.3のように固定しておくと、ある日突然起動しなくなる事態を避けられます。
ENABLED_TOOLSで使うツールだけを公開する
ENABLED_TOOLSに、カンマ区切りでツール名を並べています。この変数を指定すると、列挙したツールだけがエージェントに公開されます。起動引数の--include-toolsでも同じ指定ができます。理由は後の「ツールを絞るとコンテキストが減る」で説明します。
blastengine側は、kintone回と同じくトランザクション配信だけを有効にしています。
Claude Codeを起動して/mcpを開き、両方が接続済みになっていれば準備は完了です。
sheets側のツールを把握する
手元のmcp-google-sheets 0.6.3でtools/listを取ると、ツールは20個ありました。READMEには19個と書かれていますが、実際にはそれより1つ多いです。主なものは次のとおりです。
| 用途 | ツール |
|---|---|
| 読む |
list_sheets / get_sheet_data / get_sheet_formulas / get_multiple_sheet_data / find_in_spreadsheet
|
| 書く |
update_cells / batch_update_cells / add_rows / add_columns / batch_update
|
| ファイルを扱う |
create_spreadsheet / create_sheet / copy_sheet / rename_sheet / list_spreadsheets / search_spreadsheets / list_folders
|
| 共有する | share_spreadsheet |
今回使うのはlist_sheets、get_sheet_data、update_cells、batch_update_cellsの4つだけです。
手順1:宛先リストを書き込む
空のシートに、見出しとダミーの宛先5件をエージェントに書き込ませます。kintone回のフィールドに、差し込み用の「プラン」列を足しました。
スプレッドシート(スプレッドシートID)の「シート1」の A1:I6 に、次の値を update_cells で1回で書き込んでください。
1行目: 会社名, 担当者名, メール, プラン, 対象, ステータス, 配信ID, 送信日時, 結果メモ
2行目: 株式会社サンプル商事, 山田 太郎, yamada@example.jp, スタンダード, TRUE, 未送信, , ,
(以下、6行目まで)
update_cellsの1回で、6行×9列の54セルが更新されました。
{
"updatedRange": "'シート1'!A1:I6",
"updatedRows": 6,
"updatedColumns": 9,
"updatedCells": 54
}
5件の内訳は、対象がTRUEの行が4件、FALSEの行が1件です。TRUEの4件のうち1件は、配信可能アドレスに登録していない宛先にして、エラー時の書き戻しを確かめられるようにしました。
手順2:配信対象の行を取り出す
スプレッドシート(スプレッドシートID)の「シート1」は、1行目が見出し
(会社名, 担当者名, メール, プラン, 対象, ステータス, 配信ID, 送信日時, 結果メモ)の宛先リストです。
対象列が TRUE で、ステータス列が「未送信」の行を抽出してください。
エージェントはget_sheet_dataを範囲指定なしで1回呼び、シート全体を読んでから条件に合う行を自分で選びました。kintone MCPのkintone-get-recordsのような絞り込み条件は、get_sheet_dataにはありません。読み込んだ後の絞り込みは、エージェント側の仕事になります。
返ってきた値は次のような形です。
{
"range": "シート1",
"values": [
["会社名", "担当者名", "メール", "プラン", "対象", "ステータス", "配信ID", "送信日時", "結果メモ"],
["株式会社サンプル商事", "山田 太郎", "yamada@example.jp", "スタンダード", "TRUE", "未送信"],
…
]
}
ここから2つのことが読み取れます。
空のセルは行の末尾から省略される
配信ID・送信日時・結果メモの3列は、まだ空です。この3列は行の末尾にあるため、返ってくる配列から省略されます。Sheets APIの仕様どおりの動きです。行ごとに要素数が違う配列になるので、「9列目が空かどうか」を配列の長さで判定するような処理を書くときは注意が要ります。
TRUE / FALSEは文字列で返ってくる
「対象」列の値は、"TRUE"という文字列で返ってきました。書き込みはupdate_cellsがSheets APIのUSER_ENTERED(画面で手入力したのと同じ扱い)で行っています。そのため、シート上では真偽値として保存されています。Sheets APIを直接呼んでvalueRenderOption=UNFORMATTED_VALUEで読むと、trueという真偽値でした。
get_sheet_dataは、画面に表示される形式の値(文字列)を返します。今回の指示は「TRUE」と書いたので、そのまま一致して問題なく抽出できました。ただ、チェックボックスにした列を「チェックが入っている行」のように指示すると、何と比較すればいいかがエージェント任せになります。指示では「対象列がTRUE」のように、表示される値で条件を書いておくのが確実です。
手順3:1行ずつ差し込んで送る
抽出した各行に、blastengineのトランザクション配信で1件ずつ案内メールを送ってください。
- 送信元: info@example.com(送信者名: 株式会社example)
- 件名: 【ご案内】ご契約プランの更新についてのお知らせ
- 本文(テキスト): 「{会社名}\n{担当者名} 様\n\nいつもお世話になっております。株式会社exampleです。\n現在ご利用中の「{プラン}」プランについて、更新のご案内をお送りします。\n」({}はその行の値で置き換える)
エージェントはblastengine_send_transactionを4回呼びました。差し込みはエージェントが本文を組み立てて行います。
{
"from_email": "info@example.com",
"from_name": "株式会社example",
"to": "sato@example.jp",
"subject": "【ご案内】ご契約プランの更新についてのお知らせ",
"text_part": "検証工業株式会社\n佐藤 花子 様\n\nいつもお世話になっております。株式会社exampleです。\n現在ご利用中の「ライト」プランについて、更新のご案内をお送りします。\n"
}
送った後にGET /api/v1/deliveries/{delivery_id}で各配信の本文を確認しました。4件とも、その行の会社名・担当者名・プランが正しく入っていました。
blastengineの一斉配信にも差し込みの機能はあります。ただ、行と配信IDを1対1で対応させて書き戻したい今回の用途では、トランザクション配信で1件ずつ送るほうが扱いやすいです。
手順4:配信結果を確かめてからシートに書き戻す
判定の考え方はkintone回と同じです。配信IDが返っただけでは届いたとは限りません。また、blastengine_delivery_getのstatusは配信処理の段階を表す値です。届いたかどうかの判定は、blastengine_mail_results_listで引いた宛先ごとのstatusとlast_response_codeで行います(詳しくはkintoneの記事を参照してください)。
指示は次のとおりです。
送信APIが受け付けたら、すぐに「送信済み」にはせず、配信ログで実際の配信結果を確認してください。
- 配信に成功していたら: 配信ID列に配信ID、送信日時列に送信日時を入れ、ステータス列を「送信済み」
- DROPやエラーだったら: 配信ID列と送信日時列を入れたうえでステータス列を「エラー」、結果メモ列に結果コードとエラー内容
- 送信API自体がエラーを返したら: ステータス列を「エラー」、結果メモ列にエラー内容
書き戻すのはステータス・配信ID・送信日時・結果メモの列だけにしてください。
最後に、更新後の一覧(行番号・会社名・ステータス・配信ID・送信日時・結果メモ)を報告してください。
今回も配信ログの反映待ちがあった
4件を送った直後にblastengine_mail_results_listを引くと、配信ID 9だけ{"data":[]}が返りました。エージェントは配信ID 9について引き直しを続け、blastengine_delivery_getでも状態を確かめました。そのうえで、SENT / 250を確認してから書き戻しています。
配信ID 9の配信ログは、作成時刻が19:11:58、更新時刻が19:12:03でした。今回は5秒ほどで確定しています。kintone回でも同じ現象が起きたので、送った直後のログが空になるのは珍しいことではないと考えておいたほうがよさそうです。
書き戻しはbatch_update_cellsの1回
エージェントはbatch_update_cellsで、4行分のF〜I列をまとめて更新しました。
{
"spreadsheet_id": "(スプレッドシートID)",
"sheet": "シート1",
"ranges": {
"F2:I2": [["送信済み", 8, "2026-10-29T19:11:57+09:00", ""]],
"F3:I3": [["送信済み", 9, "2026-10-29T19:11:58+09:00", ""]],
"F4:I4": [["送信済み", 10, "2026-10-29T19:12:00+09:00", ""]],
"F5:I5": [["エラー", 11, "2026-10-29T19:12:00+09:00", "554(rejection) 宛先が配信可能なアドレスではありません。配信可能アドレスの登録状況をご確認ください。"]]
}
}
範囲を行ごとにF〜I列に限っているので、会社名やメールの列には触れていません。指示に「書き戻すのは〜の列だけ」と入れておくと、このように範囲を絞った書き方になります。
送信日時の列には、配信ログのdelivery_timeがそのまま+09:00付きの文字列で入りました。kintoneの日時フィールドは秒を保持しないので、kintone回では分単位に丸められていました。シートでは秒まで残ります。並べ替えや日付計算に使いたい場合は、日時として認識される形(2026/10/29 19:11:57など)で書くよう、指示で決めておきます。
ここまで(抽出・送信・確認・書き戻し)で13ターン、約0.10ドル(Sonnet)でした。
書き戻した後のシートは次のとおりです。
| 行 | 会社名 | 対象 | ステータス | 配信ID | 結果メモ |
|---|---|---|---|---|---|
| 2 | 株式会社サンプル商事 | TRUE | 送信済み | 8 | |
| 3 | 検証工業株式会社 | TRUE | 送信済み | 9 | |
| 4 | テスト物産株式会社 | TRUE | 送信済み | 10 | |
| 5 | 例示システム株式会社 | TRUE | エラー | 11 | 554(rejection)宛先が配信可能なアドレスではありません。… |
| 6 | 架空サービス株式会社 | FALSE | 未送信 |
Sheets APIとblastengineのAPIを直接呼んで、この表がエージェントの報告と一致していることを確認しました。
行番号は「シートの行番号」で報告させる
報告の表は正しかったのですが、表の前の要約文に「1件(4行目)はDROPで『エラー』にしています」とありました。DROPになったのはシートの5行目で、見出しを除いたデータとしては4件目です。データの何件目かと、シートの行番号が混ざっています。
人が後でシートを開いて確認する場面では、この取り違えが混乱のもとになります。報告を求めるときは「シートの行番号(見出しを1行目とする)で」と指定しておくと確実です。
同じ指示をもう一度実行しても二重に送らない
取り出す条件を「ステータスが『未送信』」にしているので、送信済みとエラーの行は次回の対象から外れます。エラーの行は、メールアドレスを直してステータスを「未送信」に戻せば、次の実行で送り直されます。新しい宛先は、シートの末尾に「未送信」で1行足すだけです。
ツールを絞るとコンテキストが減る
ENABLED_TOOLSを指定した理由の1つは、エージェントに渡すツール定義の量を減らすことです。claude -pに「OKとだけ返して」とだけ指示し、sheetsサーバーの設定を変えて、返ってきた入力トークン数を比べました。
| 構成 | 入力トークン | MCPなしとの差 |
|---|---|---|
| MCPサーバーなし | 5,462 | ― |
| sheetsの全20ツール | 13,898 | +8,436 |
ENABLED_TOOLSで4ツールに絞る |
7,239 | +1,777 |
全ツールを公開すると、何もしなくても毎回8,000トークンあまりをツール定義に使います。READMEには「約13Kトークン」とありますが、今回の計測ではそれより少ない値でした。4ツールに絞ると、その分が約79%減りました。
もう1つの理由は、使わせたくないツールを最初から見せないことです。全ツールの中にはshare_spreadsheetがあり、エージェントが任意のメールアドレスにシートを共有できます。宛先リストのシートには顧客のメールアドレスが並んでいるので、このツールは配信の作業には不要なうえ、誤って使われたときの影響が大きいものです。
Claude Codeの--allowedToolsやpermissions.denyでも、ツールの実行は止められます。ただし、それらはツール定義をエージェントに見せたうえで実行を止める仕組みです。ENABLED_TOOLSなら、サーバー側でそもそも公開しないので、コンテキストも減り、使われる余地もなくなります。
定期実行に載せる場合は、Claude Code側でも許可するツールを明示しておきます。
claude -p "(手順2〜4をまとめた指示)" \
--mcp-config .mcp.json --strict-mcp-config --tools "" \
--allowedTools "mcp__sheets__get_sheet_data,mcp__sheets__batch_update_cells,mcp__blastengine__blastengine_send_transaction,mcp__blastengine__blastengine_delivery_get,mcp__blastengine__blastengine_mail_results_list"
非対話での実行方法は週次レポートの記事、送信の直前に承認を挟む方法は誤送信を防ぐ記事で扱っています。
つまずきやすいポイント
| 症状 | 原因と対処 |
|---|---|
uvx mcp-google-sheetsでNo module named 'mcp.server.fastmcp'
|
依存のMCP Python SDKが2.x系になり、FastMCPが改名されている。uvx --with "mcp<2" mcp-google-sheets@0.6.3で1.x系に固定する |
create_spreadsheetが403 storageQuotaExceeded
|
サービスアカウントはファイルを所有できない。シートは人が作り、サービスアカウントに編集者として共有する |
| シートが見つからない・権限エラーになる | サービスアカウントのメールアドレスにシートを共有していない。サービスアカウントから見えるのは共有されたファイルだけ |
| サービスアカウントの鍵を作れない | 組織のポリシーで鍵の作成が禁止されている。管理者に相談する |
| ツールの数がREADMEと合わない | 0.6.3の実際のツールは20個。手元のtools/listで確認する |
| 空のはずの列が配列にない | 行の末尾の空セルは返り値から省略される |
チェックボックスの列が"TRUE"で返る |
get_sheet_dataは表示形式の値を返す。条件は「TRUE」のように表示される値で書く |
| 報告の行番号がずれる | データの何件目かとシートの行番号が混ざることがある。「シートの行番号で」と指定する |
| 送った直後に配信ログが空 | 反映まで数秒〜10秒ほどかかることがある。少し待って引き直す |
554(rejection)でDROPになる |
宛先が配信可能アドレスに登録されていない。トライアルでは登録した宛先にしか送れない |
| 会話の前からコンテキストを多く使っている | 全ツールの定義で8,000トークンあまりを使う。ENABLED_TOOLSで必要なツールだけを公開する |
まとめ
スプレッドシートとblastengineを1つのエージェントにつなぐ構成のポイントは3つです。
- mcp-google-sheetsは、
uvx --with "mcp<2"でMCP SDKを1.x系に固定して起動する。認証はサービスアカウントで、シートは人が作って共有する -
get_sheet_dataには絞り込み条件がなく、値は表示形式の文字列で返る。末尾の空セルも省略されるので、抽出の条件は表示される値で具体的に書く -
ENABLED_TOOLSで使うツールだけを公開すると、ツール定義のコンテキストが約8,400トークンから約1,800トークンに減り、share_spreadsheetのような不要なツールも見せずに済む
書き戻しの判定をblastengineの配信ログで行う点は、kintone回と同じです。シートを開くだけで、誰に送って誰に届かなかったかが分かる状態にできます。
blastengine MCPサーバーへの要望や感想は、以下のアンケートフォームから送ると開発チームに届くみたいです。
https://docs.google.com/forms/d/e/1FAIpQLSdNZ9TswUT3JEv3pMHkJGqOvgmsXuKP1smkJiNEsqbTVDwRyg/viewform
※本記事の手順は、mcp-google-sheets 0.6.3(MCP Python SDK 1.28.1、Python 3.14)とblastengine MCPサーバー v0.2.2を接続し、Claude Code(Sonnet)からシートへの書き込み、行の抽出、実送信、配信結果の確認、書き戻しまで通しで確認しました。提供ツールや引数はバージョンによって変わるため、手元のtools/listを確認してください。