MCPサーバー
概要
TesseraはMCP(Model Context Protocol)サーバーとして動作し、Claude Code・CursorなどのAIエージェントから、スキーマ(コンテンツタイプ定義)とコンテンツの両方を直接操作できます。
汎用LLMラッパーとの違いは、Tesseraが「CMSの構造そのもの」を理解した状態でツールを提供している点です。たとえばスキーマ変更の提案(propose_content_type_change)は、既存のフィールド構成・既存コンテンツの実データを踏まえて検証されます。「必須フィールドに変更しようとしたが、既存コンテンツに値が入っていないものがある」といったケースは、AIが気づかなくてもTessera側が機械的に検知して提案を拒否します(詳細は下記「エラーケース」参照)。
Claude Code/Cursorに接続すると、AIエージェントは以下のようなことができるようになります。
- 既存のスキーマ・コンテンツを参照しながら、コンテンツタイプの新規作成・変更・削除を提案する
- コンテンツの作成・検索・更新・削除を行う
- 既存コンテンツがスキーマ定義と整合しているか(不整合な値・参照切れ等)をオンデマンドで検査する
- 下書きコンテンツのプレビュー用リンクを発行する
- 画像・PDF等のメディアアセットをアップロードする
スキーマ変更は常にステージングされ、AIエージェントが直接確定させることはできません。 propose_content_type_changeはバリデーションを通過しても即座には反映されず、必ず承認待ち(pending_approval)としてステージングされます。反映には、管理画面(/entries)上での人間による承認が必須です。承認・却下自体はMCPツールとして提供していません(意図的な設計)。
クイックスタート
/settings/api-keysから「+ APIキーを発行」を選択し、スコープ(read_only / read_write)を選んでAPIキーを発行します。read_writeを選ぶと、コンテンツのCRUDに加えてスキーマ変更提案(propose_content_type_change)も実行できるようになります。コンテンツタイプ単位で個別に権限を上書きしたい場合(例: 特定のコンテンツタイプだけ書き込み禁止にする)は、発行画面の「コンテンツタイプ単位の例外を追加」から設定できます。この設定は発行後もAPIキー一覧画面から追加・変更・削除が可能です。- Claude Code・CursorなどMCP対応のAIエージェント側の設定ファイル(
.mcp.json)に以下を追記します。
{
"mcpServers": {
"tessera": {
"type": "http",
"url": "https://YOUR_WORKSPACE.tesseracms.example.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}- 設定ファイルを保存し、Claude Code/Cursorを再起動(または再読み込み)します。
/mcpコマンドでtesseraへの接続状態を確認できます。 - 接続後、read_writeスコープのキーであれば、AIエージェントからコンテンツの作成・更新・削除、コンテンツタイプ(スキーマ)の変更提案が行えます。read_onlyスコープのキーでは、書き込み系ツールはそもそもツール一覧に表示されません(AIエージェントに「書き込みができる」という誤った期待を持たせない設計です)。
- 濫用防止のため、MCP経由の呼び出しには回数の上限があります(プランにより異なります。詳細は下記「コスト・利用上限」参照)。MCP経由の操作はLLM呼び出しを一切行わないため、AIクレジットの消費対象にはなりません。
利用可能なツール一覧(14種類)
読み取り系(7種類・read_onlyキーでも利用可能)
| ツール | 説明 |
|---|---|
| list_content_types | ワークスペース内の承認済みコンテンツタイプ定義を一覧取得する |
| get_content_type | apiIdを指定して承認済みコンテンツタイプ定義を1件取得する |
| list_pending_proposals | 承認待ち(pending)のスキーマ変更提案を一覧取得する |
| get_proposal_status | 提案IDを指定してスキーマ変更提案の状態を取得する |
| check_content_consistency | 既存コンテンツが現在のスキーマ定義と整合しているかを検査する(型不一致・required未充足・unique違反・選択肢外の値・参照切れを検出) |
| list_entries | 指定コンテンツタイプのコンテンツを一覧取得する |
| search_entries | コンテンツのフィールド値に対する部分一致検索を行う |
| get_entry | コンテンツIDを指定して1件取得する |
スキーマ操作系(read_writeキー、または対象コンテンツタイプへの書き込みオーバーライドが必要)
| ツール | 説明 |
|---|---|
| propose_content_type_change | コンテンツタイプの新規作成・更新・削除を提案する(常にステージングのみ、即時反映なし) |
書き込み系(read_writeキー、または対象コンテンツタイプへの書き込みオーバーライドが必要)
| ツール | 説明 |
|---|---|
| create_entry | コンテンツを新規作成する(公開予約日時の指定も可能) |
| update_entry | 既存コンテンツのフィールド値・公開状態を更新する(部分マージ) |
| delete_entry | コンテンツをソフトデリートする(status: archived への変更。物理削除はしない) |
| create_preview_link | 下書きを含む任意ステータスのコンテンツに、失効可能な期限付きプレビューリンクを発行する |
| upload_asset | Base64エンコードしたファイルからメディアアセットを新規作成する(サイズ上限5MB、画像/PDF/動画/テキスト系のみ) |
list_content_types / get_content_typeは、コンテンツタイプ単位のオーバーライドで「アクセス遮断(none)」が設定されている対象を一覧・単体取得の両方から除外します(存在自体を見せません)。
実例
例1: スキーマ変更を提案し、承認画面へ直接遷移する
「blogPostにfeatured(真偽値、注目記事フラグ)を追加して」とAIエージェントに伝えると、propose_content_type_changeが呼ばれます。
// リクエスト(AIエージェント→Tessera)
{
"mode": "update",
"targetContentTypeApiId": "blogPost",
"basedOnVersion": 3,
"definition": { "apiId": "blogPost", "displayName": "ブログ記事", "fields": [ /* 既存フィールド + featured */ ] }
}// レスポンス
{
"status": "pending_approval",
"proposalId": "b1e2c3d4-...",
"mode": "update",
"targetContentTypeApiId": "blogPost",
"approvalUrl": "https://YOUR_WORKSPACE.tesseracms.example.com/entries#proposal-b1e2c3d4-...",
"impact": { /* 影響分析: 型変更・必須化・削除等がある場合、影響を受ける既存コンテンツ件数を含む */ }
}approvalUrlは、承認待ちの当該提案が表示された状態の管理画面へ直接遷移できるリンクです。人間はこのリンクを開いて差分を確認し、承認・却下を行います(承認操作自体はMCPツールとして提供していません)。
例2: 既存コンテンツとスキーマの不整合を検知する
「blogPostのデータに問題がないか確認して」と伝えると、check_content_consistencyが呼ばれます。
check_content_consistency({ contentTypeApiId: "articles" })レスポンスの形状について: MCPプロトコル上は{ content: [{ type: "text", text: "..." }] }という形でさらに1段ラップされており、content[0].textの中身が以下のJSON文字列として埋め込まれています。以下は、その中身(ツールが返す論理的な値)を示しています。
- 正常時:
{ contentTypeApiId, issues }(issuesは不整合なしなら空配列。正常時はstatusキーが付きません) - 対象コンテンツタイプが存在しない場合:
{ status: "not_found", contentTypeApiId } - 権限がない場合:
{ status: "permission_denied", contentTypeApiId }
issues配列の各要素(ConsistencyIssue)の形状:
type ConsistencyIssueKind =
| "type_mismatch" // フィールドの型がスキーマ定義と不一致
| "required_missing" // required:trueなのに値が欠落(null/キー欠落)
| "unique_violation" // unique:trueなのに値が重複
| "select_option_invalid" // select型でoptionsに含まれない値
| "dangling_reference"; // reference型で参照先コンテンツが実在しない
interface ConsistencyIssue {
contentTypeApiId: string;
fieldApiId: string;
kind: ConsistencyIssueKind;
affectedCount: number; // 該当する不整合コンテンツの総件数
sampleEntryIds: string[]; // 最大5件のサンプルコンテンツUUID(全件ではなく代表例)
}実際のレスポンス例(unique制約違反を1件検出した場合):
{
"contentTypeApiId": "articles",
"issues": [
{
"contentTypeApiId": "articles",
"fieldApiId": "slug",
"kind": "unique_violation",
"affectedCount": 3,
"sampleEntryIds": ["0b1f2b1a-1111-...", "0b1f2b1a-2222-...", "0b1f2b1a-3333-..."]
}
]
}このツールは読み取り専用で、スキーマ・コンテンツを変更しません。スキーマ変更前後で既存データとの整合性を事前確認したい場合に使います。
例3: 下書きコンテンツのプレビューリンクを発行する
「この下書き記事を編集部内で共有したい」と伝えると、create_preview_linkが呼ばれます。
// リクエスト
{ "contentTypeApiId": "blogPost", "entryId": "e5f6...", "expiresInDays": 3 }// レスポンス
{
"previewUrl": "https://YOUR_WORKSPACE.tesseracms.example.com/preview/e5f6...?token=...",
"token": "tsr_prev_...",
"expiresAt": "2026-07-10T00:00:00.000Z",
"entryId": "e5f6...",
"contentTypeApiId": "blogPost",
"label": null
}トークンはこのレスポンスの中でのみ平文表示されます(発行後の再表示は不可、/settings/previewからいつでも失効できます)。AI経由での発行は必ず有効期限が必要で、無期限リンクは選択できません(最大90日、未指定時は7日)。
エラーケース
read_onlyスコープで書き込み系ツールを呼んだ場合
Tool not foundread_onlyキーで接続した場合、propose_content_type_change / create_entry / update_entry / delete_entry / create_preview_link / upload_assetはそもそもツール一覧に登録されません。AIエージェントがこれらのツール名を直接呼び出そうとすると、MCPプロトコルレベルの「Tool not found」エラーになります(Tessera独自のエラーメッセージではなく、ツール自体が存在しないという応答です)。
コンテンツタイプ単位のオーバーライドで、デフォルトがread_writeでも特定のコンテンツタイプだけnone(アクセス遮断)に設定している場合は、ツール自体は呼び出せますが以下のように拒否されます。
{ "status": "permission_denied", "contentTypeApiId": "internalMemo" }レート制限超過時
rate_limitedMCP経由の呼び出しには、GraphQL APIと共有の枠でレート制限があります(詳細は下記「コスト・利用上限」参照)。超過時はisError: trueのツールエラーとして返り、無駄なDB書き込み・LLM呼び出しは発生しません。
{
"status": "rate_limited",
"window": "hourly",
"limit": 30,
"message": "..."
}提案(pending)と承認済み(approved)定義の違い
list_content_types / get_content_typeは、常に承認済みのコンテンツタイプ定義のみを返します。propose_content_type_changeでフィールド追加を提案した直後にこれらのツールを呼んでも、承認が完了するまで新しいフィールドは結果に含まれません。これはバグではなく意図した設計です(未承認の変更が「もう反映された」ものとしてAIエージェントに誤認されることを防ぐため)。
同種の混乱は/schema-chat(チャットAI)でも過去に発生していました(未承認のフィールドについてAIが「既に存在します」と確定的に回答してしまう問題)。この問題自体はチャットAI側でサーバー側の決定論的な注記表示により修正済みですが、MCP接続時にAIエージェント(Claude Code/Cursor)が同様の勘違いをする可能性は設計上残っています。ツール呼び出しの結果に新しいフィールドが見当たらない場合は、list_pending_proposals / get_proposal_statusで提案がまだ承認待ちでないかを確認してください。
スキーマ変更が拒否される安全性ガード
propose_content_type_changeの検証、および承認時の処理には、以下の安全性ガードが実装されています。
- 単一コンテンツ取得時のstatusフィルタ — GraphQLで単一コンテンツをid指定で取得する際も、一覧取得と同じく公開済み(published)のみを返すよう統一しています。read_onlyキー保有者がentryIdを知るだけで下書きを取得できてしまう抜け穴を防ぐための修正です。
- required化時の既存データチェック — 既存フィールドをrequired: falseからtrueに変更する提案(または新規フィールドをいきなりrequired: trueで追加する提案)は、対象フィールドに値を持たない既存コンテンツが1件でもあれば拒否されます。
{ "status": "validation_failed", "reason": "required_field_addition_unsafe", "message": "フィールド \"summary\" をrequired(必須)に変更しようとしていますが、既に3件のコンテンツがこのフィールドに値を持たないため変更できません。既存コンテンツに値を設定してから再提案してください" } - コンテンツタイプ削除時のコンテンツ件数確認 —
propose_content_type_change(delete)自体は正常にステージングされますが、承認画面では保有コンテンツ件数が必ず表示され、人間による明示確認なしには承認できません。加えて、他のコンテンツタイプのreference型フィールドから参照されている場合は、承認自体が拒否されます(削除提案のステージングは通っても、承認APIがreason: "referenced"で拒否する二段構えです)。 - reference参照先変更時の既存値チェック — reference型フィールドの参照先コンテンツタイプ(relation.targetContentType)を変更する提案は、対象フィールドに値を持つ既存コンテンツが1件でもあれば拒否されます。
{ "status": "validation_failed", "reason": "reference_target_change_unsafe", "message": "フィールド \"author\" の参照先を\"user\"から\"organization\"へ変更しようとしていますが、既に12件のコンテンツがこのフィールドに値を保持しているため変更できません。既存コンテンツの当該フィールドをnullに整理してから再提案してください" }同様の考え方で、reference型の1件/複数件(cardinality)の変更、単体型(singleton)への変換も、既存データとの整合性が取れない場合は拒否されます。
コスト・利用上限
MCP経由の操作はAIクレジット(従量課金)の対象外です。 スキーマ変更の生成自体は、接続元のAIエージェント(Claude Code/Cursor等)側のモデルが行っており、Tesseraは確定済みの定義を受け取って検証・ステージングするだけです。Tessera側でLLM APIを呼び出す処理は/schema-chat(チャットAI)のみが対象で、MCP経由のツール呼び出しにLLM呼び出しは一切発生しません。
濫用防止・DB保護のため、回数ベースのレート制限があります(AIクレジットの消費量とは無関係です)。
| 種別 | 対象ツール | Free(1時間/1日) | Paid(1時間/1日) |
|---|---|---|---|
| 読み取り系 | 上記7ツール(GraphQL Queryと共有の枠) | 300 / 2,000 | 1,200 / 10,000 |
| スキーマ提案・アセットアップロード | propose_content_type_change / upload_asset | 30 / 200 | 120 / 1,000 |
| うち削除提案(delete) | propose_content_type_change(mode: delete) | 10 / 時間 | 30 / 時間 |
| コンテンツ書き込み系 | create_entry / update_entry / delete_entry / create_preview_link(GraphQL Mutationと共有の枠) | 30 / 200 | 120 / 1,000 |
読み取り系はGraphQL Queryと、コンテンツ書き込み系はGraphQL Mutationと、それぞれ同一の枠を共有します(チャネルを跨いで合算枠を突破できない設計です)。数値は将来の実測データに基づき見直される場合があります。
関連ドキュメント
- 認証・APIキー — セッションCookie/APIキーの認証方式、スコープ設計、コンテンツタイプ単位のオーバーライドについて
- 料金・クレジット — Free/Paidプランの詳細、AIクレジットの考え方について
- GraphQL API — MCPと並ぶもう一つのAPI接続方法について
- よくある質問(承認フローの安全性について)