はじめに
2026年8月25日付で、Notion API のチェンジログに Notion MCP(notion-update-page / update_content)のページ内容更新に関する破壊的変更が2件追加されました。
- ① バッチ内の置換処理が「全て適用 or 全く適用しない」の原子的(atomic)な動作に変更
- ②
old_strに空文字列を渡すと、これまでの「暗黙スキップ」からバリデーションエラーに変更
どちらも一見地味な仕様変更ですが、Notion MCP を使って AI エージェントにページ編集をさせている人(Claude や ChatGPT のツール連携、自作の Notion 自動化スクリプトなど)にとっては、エラーハンドリングのロジックを見直さないとサイレントなデータ破損や意図しない失敗につながる重要な変更です。
📌 影響を受ける人
- Notion MCP 経由で
notion-update-pageを呼び出しているAIエージェント/自動化ツールの開発者update_contentで「削除→再挿入」のような複数オペレーションを1回のバッチにまとめている実装- Notion の「ページ内容をMarkdownとして更新するAPI」を直接叩いている実装
変更の全体像
今回の変更は「部分的に失敗しても成功扱いになる」という危険な旧挙動を廃止し、編集をトランザクション的に扱う方向への修正です。
この変更により、「あるオペレーションで内容を削除し、別のオペレーションで再挿入する」ようなバッチで、削除だけ成功して再挿入が失敗し、内容が消えたまま気づけないという事故が原理的に起きなくなります。
変更内容
| # | 変更内容 | 旧挙動 | 新挙動(2026/8/25〜) |
|---|---|---|---|
| 1 | バッチ内の置換の適用単位 | 一致した置換のみ部分適用し success を返す |
全て一致すれば全適用、1つでも不一致なら全く適用しない(原子的) |
| 1 |
old_str が不一致だった場合 |
該当箇所だけ静かにスキップ | 不一致の old_str を名指しした validation_error を返す |
| 2 |
old_str に空文字列を指定した場合 |
無視され、その置換は暗黙的にスキップ |
validation_error でバリデーション失敗 |
| - |
old_str と new_str が同一の場合 |
- | 一致確認をせず無視(挙動変更なし) |
対象は以下のツール/APIです。
-
Notion MCPのnotion-update-page(update_contentコマンド) - Notion API「ページ内容をMarkdownとして更新する」エンドポイント(②のみ)
⚠️ Breaking Change
従来「一部のold_strが古くてマッチしなくても、他の変更は保存されるので実質問題ない」と考えて設計されたリトライ処理・エラー握りつぶし処理は、今後は必ずvalidation_errorとして表面化します。逆に言えば、これまで気づかずに発生していた「部分適用によるデータ消失」のリスクは解消されます。
影響と対応
エラー発生時にどう振る舞うべきか、判断フローにするとこうなります。
具体的に必要な対応は次の3点です。
-
エラーハンドリングの追加:
notion-update-page呼び出し後にsuccessだけを見て「変更が反映された」と判断していた実装は、validation_errorを明示的にハンドリングするよう修正する。 -
リトライ戦略の見直し: バッチが原子的になったため、失敗時はページが未変更のまま残ります。部分的な巻き戻し処理は不要になり、単純に「最新のページ内容を取得 →
old_strを作り直して再送」で安全にリトライできます。 -
old_strの事前バリデーション: 呼び出し前にold_strが空文字列でないことをチェックするガード処理を追加する。特に、動的に生成した置換リストの中に空文字列が紛れ込むケース(差分抽出ロジックのバグなど)は要注意です。
💡 Tips
old_strとnew_strが完全に同一のオペレーションは、そもそも一致確認自体が行われず無視されます。「変更なしの空振りオペレーション」を安全のためにバッチに含めておいても副作用はありません。
コード例
「削除してから別オペレーションで再挿入する」典型的なバッチ処理を例に、Before/After を比較します。
Before(旧挙動: 部分適用が起きうる)
response = notion_mcp.notion_update_page(
page_id=page_id,
update_content=[
{"old_str": "## 旧セクション\n本文A", "new_str": ""}, # ① 削除
{"old_str": "", "new_str": "## 旧セクション\n本文A(更新版)"}, # ② 再挿入のつもり
],
)
if response["status"] == "success":
print("更新完了") # ← ②は old_str="" のため常にスキップされ、
# 本文Aが消えたまま success が返っていた
この例では ② の old_str が空文字列のため暗黙的にスキップされ、①の削除だけが適用されて success が返ります。呼び出し側は何も気づけません。
After(新挙動: 事前チェック + エラーハンドリング必須)
operations = [
{"old_str": "## 旧セクション\n本文A", "new_str": ""},
{"old_str": "## 旧セクション\n本文A", "new_str": "## 旧セクション\n本文A(更新版)"},
]
# 事前チェック: old_str の空文字列を弾く
for op in operations:
if op["old_str"] == "":
raise ValueError(f"old_str が空文字列です: {op}")
response = notion_mcp.notion_update_page(page_id=page_id, update_content=operations)
if response["status"] == "validation_error":
# ページは未変更なので、最新内容を取得してバッチを作り直し再送すればよい
latest_page = notion_mcp.get_page(page_id=page_id)
operations = rebuild_operations(latest_page, target_changes)
response = notion_mcp.notion_update_page(page_id=page_id, update_content=operations)
elif response["status"] == "success":
print("バッチ全体が原子的に反映された")
old_str を空文字列にする代わりに、削除対象と挿入対象を同じ old_str(=現在のページ内容)を起点にした1つの置換にまとめることで、原子的な適用のメリットを活かせます。
まとめ
-
update_contentはバッチ全体が原子的(all-or-nothing)に変更され、一部のold_strが不一致でもvalidation_errorが返り、ページは未変更のまま安全に保たれる。 -
old_strの空文字列指定は禁止となり、これまでの暗黙スキップからバリデーションエラーへ変更(notion-update-pageと Markdown更新APIの両方が対象)。 -
old_strとnew_strが同一のオペレーションは引き続き無視される。 - Notion MCP を使うAIエージェントや自動化ツールでは、①
validation_errorのハンドリング追加、②失敗時は最新内容から再構築して再送するリトライ設計、③old_strの非空チェック、の3点を優先的に見直すことを推奨します。
これらの変更は、Notion MCP を安全に運用する上で「サイレントな部分失敗」を防ぐ前向きな仕様修正です。既存の呼び出しロジックに部分適用を前提にしたエラー握りつぶしがないか、一度点検しておくとよいでしょう。