rebase generation
This commit is contained in:
178
tools/mcp/prisma-validator.mjs
Normal file
178
tools/mcp/prisma-validator.mjs
Normal 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 });
|
||||
Reference in New Issue
Block a user