@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
text/typescript
// 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);
});