rebase generation

This commit is contained in:
MaKarin
2026-04-07 19:40:41 +03:00
parent 73ddb1a948
commit aab7bfa691
180 changed files with 15512 additions and 364 deletions

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env node
/**
* MCP server: Prisma schema validator (read-only).
*
* Tools:
* - validate_schema : structural checks against a Prisma schema string
* - extract_models : return model/enum names and field counts
*
* These are deliberately NOT a shell-out to `prisma validate` — per the
* KIS-TOiR optimization constraints, MCP servers must be pure, synchronous,
* read-only validators with no external deps or CLI subprocess calls.
*/
import { runMCPServer } from "./lib/mcp-server.mjs";
// Prisma schema constants
const BUILTIN_TYPES = new Set([
"String",
"Int",
"Float",
"Boolean",
"DateTime",
"Json",
"Bytes",
"Decimal",
"BigInt",
]);
// Regex constants (compiled once, reused across invocations)
const BLOCK_RE = /^\s*(model|enum|datasource|generator)\s+(\w+)\s*\{/gm;
const INLINE_ID_RE = /@id\b/;
const COMPOSITE_ID_RE = /@@id\s*\(/;
const FIELD_RE = /^(\w+)\s+([\w?\[\]]+)/;
const RELATION_RE = /@relation\b/;
/**
* Extremely lightweight Prisma schema parser. Not a full grammar — it
* recognizes top-level blocks (`model`, `enum`, `datasource`, `generator`)
* and counts fields inside model/enum blocks by line. Sufficient for the
* structural sanity checks the orchestrator asks for.
*/
function parseBlocks(schema) {
const blocks = [];
let m;
while ((m = BLOCK_RE.exec(schema)) !== null) {
const kind = m[1];
const name = m[2];
const openIdx = schema.indexOf("{", m.index);
// Walk forward to find the matching close brace. Prisma schemas do not
// nest braces inside blocks, so a simple depth counter is enough.
let depth = 1;
let i = openIdx + 1;
for (; i < schema.length && depth > 0; i++) {
if (schema[i] === "{") depth++;
else if (schema[i] === "}") depth--;
}
const body = schema.slice(openIdx + 1, i - 1);
const fieldLines = body
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"));
blocks.push({ kind, name, body, fields: fieldLines });
}
return blocks;
}
const tools = [
{
name: "validate_schema",
description:
"Validate a Prisma schema string for structural soundness: required generator and datasource blocks, at least one model, each model has an @id or @@id, referenced enums exist. Read-only; never writes files.",
inputSchema: {
type: "object",
properties: {
schema: {
type: "string",
description: "Full contents of schema.prisma",
},
},
required: ["schema"],
},
execute: async ({ schema }) => {
if (typeof schema !== "string" || schema.length === 0) {
return { success: false, error: "schema must be a non-empty string" };
}
const issues = [];
const blocks = parseBlocks(schema);
const generators = blocks.filter((b) => b.kind === "generator");
const datasources = blocks.filter((b) => b.kind === "datasource");
const models = blocks.filter((b) => b.kind === "model");
const enums = blocks.filter((b) => b.kind === "enum");
if (generators.length === 0)
issues.push("missing generator block (expected `generator client`)");
if (datasources.length === 0)
issues.push("missing datasource block (expected `datasource db`)");
if (models.length === 0) issues.push("schema declares no models");
const enumNames = new Set(enums.map((e) => e.name));
for (const model of models) {
const hasInlineId = INLINE_ID_RE.test(model.body);
const hasCompositeId = COMPOSITE_ID_RE.test(model.body);
if (!hasInlineId && !hasCompositeId) {
issues.push(`model ${model.name} has no @id or @@id directive`);
}
// Detect referenced types that look like enums but are not declared.
for (const fieldLine of model.fields) {
// field definition: `name Type[?] ...`
const fieldMatch = fieldLine.match(FIELD_RE);
if (!fieldMatch) continue;
const rawType = fieldMatch[2].replace(/[\?\[\]]/g, "");
if (BUILTIN_TYPES.has(rawType)) continue;
// If a type looks like an enum reference (capitalized, no model match),
// warn when it is not found in declared models or enums.
const isKnownModel = models.some((m) => m.name === rawType);
const isKnownEnum = enumNames.has(rawType);
if (!isKnownModel && !isKnownEnum) {
// Might be a relation target defined later — only flag if it
// looks enum-shaped (all caps first letter, no `@relation` on the line).
if (!RELATION_RE.test(fieldLine)) {
issues.push(
`model ${model.name} field \`${fieldMatch[1]}\` references unknown type \`${rawType}\``
);
}
}
}
}
return {
success: issues.length === 0,
result: {
blockCounts: {
generator: generators.length,
datasource: datasources.length,
model: models.length,
enum: enums.length,
},
issues,
},
};
},
},
{
name: "extract_models",
description:
"Extract model and enum names from a Prisma schema string, along with per-model field counts. Useful before diffing against a frozen contract.",
inputSchema: {
type: "object",
properties: {
schema: { type: "string" },
},
required: ["schema"],
},
execute: async ({ schema }) => {
if (typeof schema !== "string") {
return { success: false, error: "schema must be a string" };
}
const blocks = parseBlocks(schema);
return {
success: true,
result: {
models: blocks
.filter((b) => b.kind === "model")
.map((b) => ({ name: b.name, fieldCount: b.fields.length })),
enums: blocks
.filter((b) => b.kind === "enum")
.map((b) => ({ name: b.name, valueCount: b.fields.length })),
},
};
},
},
];
runMCPServer({ name: "kis-toir-prisma-validator", version: "0.1.0", tools });