固定コアは手書き SQL migration、拡張レイヤ(meta jsonb)は MCP の db_schema_addField で誰でも 即追加できる。schema 系ツール(addField / describe / migration_propose)は manifest だけで動くので Neon 未接続の dev でも完動する(行データ操作だけが DATABASE_URL を要求する)。
1. 依存と機能
apps/<app>/package.json に "@event/crm-schema": "" と "@event/mcp": ""、next.config.ts の transpilePackages にも両方を足す。event.config.ts で db: true と mcp: true、db descriptor の nav に /admin/db を登録する:
{
key: "db",
label: "リレーショナル DB(MCP-first)",
description: "core は手書き migration、extension は MCP の db_schema_addField で追加",
nav: [{ id: "db-schema", group: "システム", label: "DB スキーマ", href: "/admin/db", code: "DB", order: 2 }],
env: [
{ name: "DATABASE_URL", required: false, description: "行データ操作に必要(schema/addField は不要)" },
{ name: "DATABASE_URL_UNPOOLED", required: false, description: "migration 適用用の直接接続" },
],
manualChapters: [15],
},
2. Neon を接続(行データを扱うときのみ)
Vercel Marketplace か Neon で DB を作り、env に:
DATABASE_URL=postgres://...@...pooler...neon.tech/... # runtime(pooled)
DATABASE_URL_UNPOOLED=postgres://...@...neon.tech/... # migration 用(直接)
未設定でも 4〜7 の schema 操作(MCP でカラムを足す)は動く。DB を触るのは 3 と行データ操作だけ。
3. 固定コアを適用
@event/crm-schema/migrations/0001_init.sql を pg 経由で適用する (drizzle-kit push は TTY 要求 / neon-http は multi-statement 非対応のため使わない)。 apply スクリプト apps/<app>/scripts/apply-migrations.mjs:
import { readFileSync, readdirSync } from "node:fs";
import { Client } from "pg";
const url = process.env.DATABASE_URL_UNPOOLED || process.env.DATABASE_URL;
const dir = "node_modules/@event/crm-schema/migrations";
const c = new Client({ connectionString: url });
await c.connect();
for (const f of readdirSync(dir).filter((x) => x.endsWith(".sql")).sort()) {
process.stdout.write(`→ ${f} ... `);
await c.query(readFileSync(`${dir}/${f}`, "utf8"));
console.log("ok");
}
await c.end();
node --env-file=apps/<app>/.env.local apps/<app>/scripts/apply-migrations.mjs で実行。 (org/member/user/entity_lp/audit_log + compliance テーブル + 追記専用 trigger が作られる。冪等。)
4. manifest を content に配線(Neon 不要で動く核)
apps/<app>/src/lib/db.ts — db(lazy Neon)と、Field Manifest を content/db-manifest.json に 持つ ManifestStore:
import "server-only";
import { db as crmDb, DEFAULT_MANIFEST, type FieldManifest } from "@event/crm-schema";
import type { ManifestStore } from "@event/mcp";
import { store } from "@/lib/cms"; // @event/core contentStore(dev=fs / 本番=GitHub commit)
export const db = crmDb;
// schema.addField は manifest への追記なので DB に触れない = dev でも即時に効く。
// 本番では contentStore が GitHub commit になる = 「カラム追加」が PR 履歴に残る。
export const manifestStore: ManifestStore = {
read: () => store.readResourceFresh<FieldManifest>("db-manifest", DEFAULT_MANIFEST),
async write(next) {
await store.writeResource("db-manifest", next, "chore(db): schema.addField via MCP/admin");
},
};
行の更新は必ず guardedUpdate({ db, table, manifest: createManifestIndex(await manifestStore.read()), entity, id, patch, writer, expectedVersion, actor }) を通す(version 楽観ロック + audit + core/meta 振り分け)。
5. HTTP MCP に db ツールを配線(URL 経由で DB をいじる)
apps/<app>/src/lib/mcp.ts の buildMcpTools に足す(/api/mcp が既に buildMcpTools を使う):
import { contentToolGroup, foursToolGroup, dbToolGroup, type EventTool, type McpSettings } from "@event/mcp";
import { features } from "@/event.config";
import { db, manifestStore } from "@/lib/db";
export async function buildMcpTools(settings: McpSettings): Promise<EventTool[]> {
const tools: EventTool[] = [];
if (settings.tools.content) tools.push(...contentToolGroup({ registry, reader, store }));
if (settings.tools.fourS) tools.push(...foursToolGroup({ config: createFoursConfig() }));
// schema_addField / describe / migration_propose は Neon 不要。entity_* は DATABASE_URL 接続時のみ。
if (settings.tools.db && features.isEnabled("db")) {
tools.push(...dbToolGroup({ db, manifestStore, actor: `mcp:${settings.actorName}` }));
}
return tools;
}
content/mcp-settings.json の "tools": { "db": true, ... } を有効化(設定画面 /admin/settings/mcp でもトグル可)。HTTP を使うなら httpEnabled: true + env MCP_BEARER_TOKEN。
6. stdio MCP にも同じ db ツール(ローカル接続)
apps/<app>/scripts/mcp.mjs に足す(Claude Desktop / Code のローカル接続用。同一 dbToolGroup を共有):
import { dbToolGroup } from "@event/mcp";
import { db, DEFAULT_MANIFEST } from "@event/crm-schema";
// ...settings 読込・contentStore(store) は既存...
if (settings.tools.db) {
const manifestStore = {
read: () => store.readResourceFresh("db-manifest", DEFAULT_MANIFEST),
async write(next) { await store.writeResource("db-manifest", next, "chore(db): schema.addField via MCP"); },
};
tools.push(...dbToolGroup({ db, manifestStore, actor: `mcp:${settings.actorName}` }));
}
.mcp.json: { "mcpServers": { "<app>-db": { "command": "npx", "args": ["tsx", "scripts/mcp.mjs"], "env": { "DATABASE_URL": "..." } } } }
7. /admin/db スキーマ参照(可視化)
apps/<app>/src/app/admin/db/page.tsx — adminAuth + features.isEnabled("db") gate。 manifestStore.read() を描画し、entity × field を core / extension / writableBy / version で表示する (MCP の addField が書く先そのもの。DATABASE_URL 未設定でも manifest は読めるので dev で動く)。
8. 検証(「カラムを足す」を実際に叩く)
yarn install
yarn workspace <app> typecheck # 型が通ること
yarn workspace <app> dev
/admin/dbに Field Manifest が出る(DATABASE_URL 未設定でも)- MCP クライアントから
db_schema_addField(例: entity="organization", name="leadScore", type="number",
writableBy=["admin","mcp"])→ manifest v が上がり /admin/db に反映(Neon 無しで確認可能)
db_migration_propose→ db/migrations/proposals/ に SQL が出る(自動実行しない)- 行データを扱うなら 2〜3 で Neon を接続し
node --env-file=apps/<app>/.env.local apps/<app>/scripts/apply-migrations.mjs
で固定コアを適用 → db_entity_list / db_entity_update が通る
固定コアの適用スクリプト apps/<app>/scripts/apply-migrations.mjs(pg を devDependency に追加):
import { readFileSync, readdirSync } from "node:fs";
import { Client } from "pg";
const url = process.env.DATABASE_URL_UNPOOLED || process.env.DATABASE_URL;
const dir = "node_modules/@event/crm-schema/migrations";
const c = new Client({ connectionString: url });
await c.connect();
for (const f of readdirSync(dir).filter((x) => x.endsWith(".sql")).sort()) {
process.stdout.write(`→ ${f} ... `);
await c.query(readFileSync(`${dir}/${f}`, "utf8"));
console.log("ok");
}
await c.end();
完了条件: /admin/db に manifest が描画され、MCP の db_schema_addField で version が上がり反映される (ここまで Neon 不要)。Neon 接続後は db_entity_update が guardedUpdate 経由で通り audit に残る。