ガイド 第15

MCP-first 二層 DB

誰でも MCP でカラムを足せる Neon 層

@event/crm-schema@event/mcp

リレーショナル層は「固定コアは堅く、拡張は誰でも」。core は手書き SQL migration + 人間承認でしか 変わらず、extension は MCP の db_schema_addField で ALTER TABLE なしに即追加できる。両者を統べるのが Field Manifest(単一真実源)。実装は @event/crm-schema + @event/mcp の db ツール群。

二層構造

レイヤ置き場所変更方法誰が書ける
core実カラム(手書き migration)db_migration_propose → SQL 生成 → 人間承認 PRsystem のみ(構造変更)
extensionmeta jsonb のキーdb_schema_addField(即時・ALTER TABLE 不要)admin / mcp

全テーブルは標準 core フィールドを持つ: id / createdAt / updatedAt / deletedAt(soft-delete)/ version(楽観ロック)。これらは STANDARD_CORE_FIELDS として固定され、MCP/admin からは書けない。

Field Manifest — 3 消費者の単一真実源

FieldManifest = { version, entities: EntityDef[] }。1 つの manifest を 3 者が読む:

  1. Admin UI@event/cms の FieldSchema 生成に使う
  2. MCP serverdb_schema_describe で全 entity×field を返し、書き込みガードに使う
  3. Manual — DB リファレンスの自動生成元。生成結果は /reference/db に全 entity × field の表として公開される

createManifestIndex(manifest) が lookup とガードを提供する。中核は splitPatch(entity, patch, writer): patch のキーを core カラム書き込みmeta(extension) 書き込み に振り分け、未登録キー / 権限なし / 型・制約違反を errors に列挙する。layer(構造の区分)と writableBy(値の書き込み権限)は独立 —— core カラムでも writableBy に writer が含まれれば値は更新できるが、構造変更は常に migration_propose 経由

「カラムを足す」= addExtensionField

addExtensionField(manifest, "organization", {
  name: "leadScore", type: "number",
  writableBy: ["admin", "mcp"], description: "商談確度スコア",
});
// → { ok: true, manifest: { version: +1, ... } }  ※非破壊。version が上がる

field 名は ^[a-zA-Z][a-zA-Z0-9_]{0,62}$、既存 core/extension との重複は弾く。layer は常に "extension"この操作は DB に触れない(manifest jsonb への追記のみ)。だから Neon 未接続の dev でも即座に効く。

書き込みガード — guardedUpdate

行の更新は必ず guardedUpdate を通す:

await guardedUpdate({
  db, table, manifest,           // manifest = createManifestIndex(...)
  entity: "organization", id,
  patch: { leadScore: 82 },      // splitPatch で core/meta に自動振り分け
  writer: "mcp",                 // "system" | "admin" | "mcp"
  expectedVersion,               // 不一致は STALE で拒否(楽観ロック)
  actor: "mcp:claude",           // audit に残る
});

version 楽観ロック + updatedAt 自動刻印 + 追記専用 auditLog への記録がまとまって効く。物理削除は不可 (guardedArchivedeletedAt を刻む soft-delete のみ)。

MCP db ツール群(dbToolGroup)

dbToolGroup({ db, manifestStore, actor, entities? }) が返す EventTool[]:

tool役割Neon 要否
db_schema_describeField Manifest を返す(書き込み前に必ず参照)不要
db_schema_addField拡張レイヤに field 追加 = カラムを足す不要
db_migration_propose固定コア変更の SQL を提案(自動実行しない)不要
db_entity_list / _get行の照会(soft-delete 除外)
db_entity_updateguardedUpdate 経由の更新
db_entity_archivesoft-delete
db_audit_query監査ログ照会

ManifestStore = { read(): Promise<FieldManifest>; write(next): Promise<void> } を注入する。 starter は content/db-manifest.json に持たせる(@event/core の contentStore = dev は fs、本番は GitHub commit =「カラム追加」が PR 履歴に残る)。だから schema 系ツールは Neon なしで完動し、行データ操作だけが DATABASE_URL を要求する。

固定コアの適用

@event/crm-schema/migrations/0001_init.sqlpg で非対話適用する(drizzle-kit push は TTY 要求 / neon-http は multi-statement 非対応のため使わない)。org / user / member / entity_lp / audit_log + compliance テーブル + 追記専用 trigger が作られる。冪等。手順は recipe add-database に完全なコード付きで ある(scripts/apply-migrations.mjs)。

配線の要点

  • db は lazy Proxy(@event/crm-schema)。DATABASE_URL 未設定でも import では落ちず、実クエリ時に throw
  • HTTP MCP(/api/mcp)と stdio(scripts/mcp.mjs)は 同一 dbToolGroup を共有 — URL 経由でも

ローカル接続でも同じ「カラムを足す」操作ができる

  • /admin/db が manifest を可視化(core/extension・writableBy・version)。MCP の addField が書く先そのもの