テスト一覧

PRISM JOURNAL

CMS向けMCPのリファクタリング: CRUD設計原則

CMS向けMCPサーバーで、ポストとページを1つのツールに混在させたときに起きる問題を整理しました。公式MCPの事例をもとに、コンテンツCRUDツールをフォーマット別に分ける設計原則を説明します。

CMS向けMCPのリファクタリング: CRUD設計原則

AIエージェントがブログ記事を書き、ランディングページまで作れるMCPサーバーを設計する中で経験した試行錯誤と、SanityやNotionなどの公式MCPから学んだパターンを整理します。


課題: 「1つのツールが多くをやりすぎている」

私たちのCMSには2種類のコンテンツがあります。

  • ポスト: Editor.jsベースのブロックエディタで書くブログ記事
  • ページ: GrapesJSベースのビジュアルビルダーで作るランディングページや紹介ページ

最初は、この2つを1つのMCPツールで扱っていました。

{
  "name": "create_post",
  "description": "ポストまたはページを作成します"
}

1つのツールの中で、markdown フィールドがあればポスト、grapes フィールドがあればページとして分岐する構造でした。部分修正も同じで、8種類のoperationを1つの patch_content ツールに詰め込んでいました。

{
  "name": "patch_content",
  "operations": [
    { "op": "replace_text", "search": "...", "replace": "..." },
    { "op": "insert_block", "blockIndex": 2, "block": { "type": "paragraph", "data": { "text": "..." } } },
    { "op": "replace_section", "selector": "#hero", "html": "..." },
    { "op": "update_css", "css": "..." }
  ]
}

ブロックエディタ向けのoperationとビジュアルビルダー向けのoperationが混ざっているため、AIエージェントの立場ではどのoperationを使うべきかが分かりにくく、間違ったタイプを送ると「ポスト専用です」というエラーが返るだけでした。


調査: Sanity、Notion、Contentfulはどう設計していたか

リファクタリングの前に、主要CMSの公式MCPサーバーを調べました。

Sanity: 「AIの魔法」より「予測可能性」

Sanity MCPはv2.6.0で AI-powered mutation tools を削除し、直接的なフィールド単位のパッチに置き換えました。その理由が印象的でした。

"more predictable, easier to debug"

さらに、入力フォーマットをツール名に明示しています。

ツール用途
create_documents_from_jsonJSONでドキュメントを作成
create_documents_from_markdownMarkdownでドキュメントを作成
patch_document_from_jsonJSONで部分修正

フォーマットが違えば、ツールも分けます。1つのツールに複数フォーマットの自動判定をさせません。

Notion: 「Markdownを抽象化レイヤーにする」

Notion MCPは内部では28種類以上のブロックタイプを使っていますが、MCPツールでは Notion-flavored Markdown を入出力フォーマットに使います。ブロックJSONはトークンを10〜20倍消費するからです。

部分修正は次の2つに単純化していました。

  • update_content — テキストの検索と置換
  • replace_content — 本文全体の置き換え

8種類のoperationを並べる代わりに、明確な2つの選択肢だけを提供しています。

Contentful: 「公開は別ツール」

Contentfulは create_entryupdate_entry とは別に publish_entry を独立したツールとして分けています。状態変更をフィールド更新に混ぜない設計です。

共通パターンの整理

原則説明
フォーマット別にツールを分ける入力形式が違えばツールを分ける
予測可能性を優先する自動判定より明示的な選択
公開/削除を分離する状態遷移はCRUDとは別ツールにする
スキーマ参照を用意するエージェントが先に構造を把握できるようにする
ツール数は5〜15個を目安にするコンテキスト予算を守る

リファクタリング: 混合型からフォーマット分離型へ

Before (v2.x): 混合型3ツール

create_post     → ポストもページも扱う
update_post     → markdownもhtml/cssも受け取る
patch_content   → 8種類のoperationが1つに入っている

AIがこのツールを使うには、まずそのコンテンツがポストなのかページなのかを判断し、合うフィールドを選んで送る必要がありました。判断を誤ると、静かに無視されるか、実行時になって初めてエラーになりました。

After (v3.0): フォーマット別7ツール

作成:

ツール対象入力
create_postポストMarkdown
create_pageページHTML + CSS

本文の置き換え:

ツール対象入力
update_post_contentポストMarkdown
update_page_contentページHTML + CSS

部分修正:

ツール対象用途
patch_text共通テキストの検索と置換
patch_post_blocksポストブロックの追加・修正・削除・移動
patch_page_htmlページセクションの置き換えとCSS修正

ツール数は3個から7個に増えましたが、それぞれの入力スキーマはずっと単純になりました。AIエージェントはツール名を見るだけで、そのツールがどのフォーマットを受け取るかを判断できます。


rawContent に統合して保存する

ポストとページは同じDBスキーマを共有しています。どちらも page_translations テーブルの raw_content フィールドを使いますが、保存フォーマットは異なります。

ポスト (Editor.js):

{
  "blocks": [
    { "type": "header", "data": { "text": "タイトル", "level": 2 } },
    { "type": "paragraph", "data": { "text": "本文テキストです。" } }
  ],
  "version": "2.28.0"
}

ページ (GrapesJS):

{
  "type": "grapes",
  "builderState": { "pages": [{ "name": "Page", "component": "..." }] },
  "html": "
...
...
", "css": "#hero h1 { font-size: 3rem; } ..." }

type: "grapes" フィールドが判別子の役割を持ちます。読み込むときにこの値を見て、どのエディタを開くかを決めます。

content フィールドにはレンダリング用のHTMLを保存します。ページの場合は <style>CSS</style>HTML を1つにまとめたself-containedなHTMLにして、別途CSSを読み込まなくてもすぐに表示できるようにしました。


部分修正が難しい理由

「タイトルのテキストを変えてください」は簡単そうに見えますが、内部ではコンテンツの種類ごとにまったく違う処理になります。

ポスト (ブロックベース):

{
  "op": "update",
  "blockIndex": 0,
  "block": { "data": { "text": "新しいタイトル" } }
}

ブロックのインデックスで対象を見つけて、data フィールドをマージします。

ページ (HTMLベース):

{
  "op": "replace_section",
  "selector": "#hero",
  "html": "

新しいタイトル

" }

CSSセレクタでDOM要素を見つけて、セクションごと差し替えます。これはサーバーサイドでDOMパーサーなしに処理する必要があったため、idセレクタをベースにした正規表現マッチングを使いました。

この違いがあるため、1つの patch ツールにまとめるとどうしても複雑になります。Sanityの原則に従って分けたのは正しい判断でした。

本当に共通化できるのは テキストの検索と置換 (patch_text) だけです。これはフォーマットが違っても同じ操作だからです。


CMS向けMCPツール設計のチェックリスト

今回の作業を通して、次の設計原則に整理されました。

  1. 入力フォーマットが違うならツールを分ける。 「このツールはMarkdownだけを受け取る」のほうが、「MarkdownまたはHTML/CSSを受け取る」よりAIには明確です。
  2. 自動判定より明示的な選択を優先する。 コンテンツタイプを実行時に推論するのではなく、ツールを選ぶ時点で決まるようにします。
  3. 状態遷移は別ツールに分ける。 publisharchiveupdate に混ぜません。
  4. 部分修正はコンテンツ構造に合わせる。 ブロックエディタならブロック単位、ビジュアルビルダーならセレクタ単位で扱います。広く共通化できるのはテキスト置換だけです。
  5. 読み取りレスポンスはエージェント向けに整形する。 生のJSONをそのまま返すのではなく、すぐ使える形にして返します。
  6. ツール名そのものがドキュメントになる。 create_postcreate_pagepatch_post_blocks のように、名前だけで対象とフォーマットが伝わるようにします。

結果

ツール数は3個から7個に増えましたが、それぞれの入力スキーマが明確になったことで、AIエージェントのミスは減りました。「ポストにGrapesのフィールドを送る」や「ページにブロックoperationを送る」といった型の不一致は、構造的に起こりにくくなりました。

SanityがAI-powered mutationをやめて明示的なパッチに戻した理由を、今回のリファクタリングで実感しました。AIツールは賢いほど良いのではなく、予測可能であるほど良いのです。

#CMS MCP#MCPサーバー#コンテンツCRUD#ツール設計#リファクタリング#Sanity MCP#Notion MCP
kojaen