UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

177 lines (157 loc) • 6.15 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. /** * ADR-214 — refresh schema-mirror.json against staging or prod. * * Connects to the configured Postgres URL (env var SUPABASE_DB_URL or * DATABASE_URL) and queries pg_constraint for CHECK constraints on the * tables backing Phase 1 schema resources. Parses the constraint * definitions, extracts enum values, writes a deterministic JSON file at * mcp-server/src/resources/schema-mirror.json. * * NOT run by CI — humans run this when they suspect or know that a CHECK * constraint changed in production. The mirror is committed to git so PR * diffs surface drift. * * Usage: * SUPABASE_DB_URL=postgres://... npx tsx mcp-server/scripts/refresh-schema-mirror.ts * * Optional dep: `pg` (added as devDependency on mcp-server). */ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const MIRROR_PATH = join(__dirname, "..", "src/resources/schema-mirror.json"); interface MirrorContent { _generated_at: string; _source: string; _note: string; [table: string]: string | Record<string, string[]>; } const TABLES_TO_MIRROR: Array<{ table: string; columns: string[] }> = [ { table: "audio_files", columns: ["classification", "file_type"] }, { table: "recording_credits", columns: ["role", "attestation_status"] }, { table: "recordings", columns: ["version_type"] }, // ADR-252 WS-A: work_credits is dropped. Composition credits live on // work_collaborators, whose only CHECK enum is attestation_status (role is // validated tool-side, not by a DB constraint). Master roles → recording_credits. { table: "work_collaborators", columns: ["attestation_status"] }, { table: "works", columns: ["work_status", "work_type"] }, ]; function fail(msg: string): never { console.error(`[refresh-schema-mirror] ERROR: ${msg}`); process.exit(1); } /** * Parse a CHECK constraint definition string for ANY-of-array shapes. * Postgres formats vary; common shapes: * CHECK ((work_type = ANY (ARRAY['song'::text, 'instrumental'::text, ...]))) * CHECK ((work_type::text = ANY (ARRAY['song'::text, ...]))) * CHECK (work_type IN ('song', 'instrumental', ...)) * Returns the literal values in source order; caller sorts for determinism. */ function parseEnumFromCheckDef(def: string): string[] | null { // Match ARRAY[...] form const arrayMatch = def.match(/ARRAY\[([^\]]+)\]/); if (arrayMatch) { const inside = arrayMatch[1]; const literals = [...inside.matchAll(/'([^']+)'/g)].map((m) => m[1]); return literals.length > 0 ? literals : null; } // Match IN (...) form const inMatch = def.match(/IN\s*\(([^)]+)\)/i); if (inMatch) { const inside = inMatch[1]; const literals = [...inside.matchAll(/'([^']+)'/g)].map((m) => m[1]); return literals.length > 0 ? literals : null; } return null; } async function main(): Promise<void> { const dbUrl = process.env.SUPABASE_DB_URL ?? process.env.DATABASE_URL; if (!dbUrl) { fail( "SUPABASE_DB_URL (or DATABASE_URL) env var not set. Provide a Postgres connection string for staging or prod.", ); } // Lazy-import pg so the file type-checks even if pg isn't installed. // Generator is the hot path; refresh is operator-only. let Client: typeof import("pg").Client; try { ({ Client } = await import("pg")); } catch { fail( "pg npm package not installed. Run: cd mcp-server && npm install --save-dev pg @types/pg", ); } const client = new Client({ connectionString: dbUrl }); await client.connect(); const out: Record<string, Record<string, string[]>> = {}; for (const { table, columns } of TABLES_TO_MIRROR) { out[table] = {}; for (const col of columns) { // Find a CHECK constraint on this column. Look for any constraint // whose pg_get_constraintdef text references the column name and // contains an ANY/IN enum list. const result = await client.query<{ def: string }>( ` SELECT pg_get_constraintdef(oid) AS def FROM pg_constraint WHERE conrelid = $1::regclass AND contype = 'c' AND pg_get_constraintdef(oid) ILIKE $2 `, [`public.${table}`, `%${col}%`], ); let values: string[] | null = null; for (const row of result.rows) { const parsed = parseEnumFromCheckDef(row.def); if (parsed && parsed.length > 0) { values = parsed; break; } } if (!values) { console.warn( `[refresh-schema-mirror] warn: no enum CHECK found for ${table}.${col}; leaving empty array`, ); values = []; } out[table][col] = [...values].sort(); } } await client.end(); // Preserve metadata from existing mirror if present. let existingNote = "CHECK constraint enums for tables backing ADR-214 Phase 1 schemas. Refresh via `npx tsx mcp-server/scripts/refresh-schema-mirror.ts` against staging. Committed so PR diffs surface schema drift. See ADR-214 Decision section, Drift Prevention #3."; try { const prev = JSON.parse( readFileSync(MIRROR_PATH, "utf-8"), ) as MirrorContent; if (typeof prev._note === "string") existingNote = prev._note; } catch { // first run — fine } const sortedTables = Object.keys(out).sort(); const sortedOut: Record<string, Record<string, string[]>> = {}; for (const t of sortedTables) { const cols = Object.keys(out[t]).sort(); sortedOut[t] = {}; for (const c of cols) sortedOut[t][c] = out[t][c]; } const final: MirrorContent = { _generated_at: new Date().toISOString(), _source: dbUrl.replace(/:[^:@]+@/, ":***@"), _note: existingNote, ...sortedOut, }; writeFileSync(MIRROR_PATH, JSON.stringify(final, null, 2) + "\n", "utf-8"); console.log( `[refresh-schema-mirror] wrote ${sortedTables.length} tables to ${MIRROR_PATH}`, ); } main().catch((err) => { console.error(err); process.exit(1); });