レシピ · 8手 · 1プロンプト実装

MCP-first 二層 DB を追加する(MCP でカラムを足せる)

Neon DB を接続し、固定コア + meta 拡張の二層で、MCP から誰でもカラムを足せる状態にする。schema 操作は Neon 未接続でも dev で動く

前提: new-event解説 ch.15module: リレーショナル DBmodule: MCP 連携

固定コアは手書き 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.tstranspilePackages にも両方を足す。event.config.tsdb: truemcp: 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.tsbuildMcpTools に足す(/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

writableBy=["admin","mcp"])→ manifest v が上がり /admin/db に反映(Neon 無しで確認可能

で固定コアを適用 → db_entity_list / db_entity_update が通る

固定コアの適用スクリプト apps/<app>/scripts/apply-migrations.mjspg を 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 に残る。