Prisma Generator
- Install
- Field-Level Security
- OOM Guard & Tenant Isolation
- Wiring into Your Server
- Schema Annotations
- Generator Configuration
- Generated Output
- Requirements
A Prisma Generator that reads schema.prisma annotations and produces Vurb.ts ToolBuilders and Presenters with field-level security, tenant isolation, and OOM protection baked into the generated code.
generator mcp {
provider = "vurb-prisma-gen"
output = "../src/tools/database"
}
model User {
id String @id @default(uuid())
email String @unique
role String @default("USER")
passwordHash String /// @vurb.hide
stripeToken String /// @vurb.hide
creditScore Int /// @vurb.describe("Financial score from 0 to 1000. Above 700 is PREMIUM.")
tenantId String /// @vurb.tenantKey
}npx prisma generate
# → src/tools/database/userPresenter.ts
# → src/tools/database/userTools.tsInstall
npm install @vurb/prisma-genPeer dependencies: Vurb.ts, zod, and @prisma/generator-helper.
Field-Level Security
/// @vurb.hide physically excludes fields from the generated Zod response schema. /// @vurb.describe() compiles into .describe() calls that inject domain semantics.
model User {
id String @id @default(uuid())
email String @unique
passwordHash String /// @vurb.hide
stripeToken String /// @vurb.hide
creditScore Int /// @vurb.describe("Financial score from 0 to 1000. Above 700 is PREMIUM.")
}Generated Presenter:
// src/tools/database/userPresenter.ts (generated)
export const UserResponseSchema = z.object({
id: z.string(),
email: z.string(),
role: z.string(),
creditScore: z.number().int().describe('Financial score from 0 to 1000. Above 700 is PREMIUM.'),
// passwordHash and stripeToken are physically absent
}).strict();
export const UserPresenter = createPresenter('User')
.schema(UserResponseSchema)
.rules(['Data originates from the database via Prisma ORM.']);Prisma queries return passwordHash and stripeToken from the database. The Presenter's .strict() strips them in RAM before serialization.
OOM Guard & Tenant Isolation
/// @vurb.tenantKey injects the tenant filter into every generated query's WHERE clause. Pagination is enforced with take capped at 50.
// src/tools/database/userTools.ts (generated)
export const userTools = defineTool<PrismaVurbContext>('db_user', {
actions: {
find_many: {
readOnly: true,
description: 'List User records with pagination',
returns: UserPresenter,
params: z.object({
email_contains: z.string().optional(),
take: z.number().int().min(1).max(50).default(20)
.describe('Max rows per page (capped at 50)'),
skip: z.number().int().min(0).default(0)
.describe('Offset for pagination'),
}),
handler: async (ctx, args) => {
const where: Record<string, unknown> = {};
where['tenantId'] = ctx.tenantId;
if (args.email_contains !== undefined) {
where['email'] = { contains: args.email_contains };
}
return await ctx.prisma.user.findMany({
where,
take: args.take,
skip: args.skip,
});
},
},
find_unique: {
readOnly: true,
description: 'Get a single record by ID',
returns: UserPresenter,
params: z.object({ id: z.string() }),
handler: async (ctx, args) => {
return await ctx.prisma.user.findUniqueOrThrow({
where: { id: args.id, tenantId: ctx.tenantId },
});
},
},
create: {
description: 'Create a new record',
returns: UserPresenter,
params: z.object({
email: z.string(),
role: z.string().optional(),
passwordHash: z.string(),
stripeToken: z.string(),
creditScore: z.number().int()
.describe('Financial score from 0 to 1000. Above 700 is PREMIUM.'),
}),
handler: async (ctx, args) => {
return await ctx.prisma.user.create({
data: { ...args, tenantId: ctx.tenantId },
});
},
},
update: {
description: 'Update an existing record',
returns: UserPresenter,
params: z.object({
id: z.string(),
email: z.string().optional(),
role: z.string().optional(),
passwordHash: z.string().optional(),
stripeToken: z.string().optional(),
creditScore: z.number().int()
.describe('Financial score from 0 to 1000. Above 700 is PREMIUM.')
.optional(),
}),
handler: async (ctx, args) => {
const { id, ...data } = args;
return await ctx.prisma.user.update({
where: { id, tenantId: ctx.tenantId },
data,
});
},
},
delete: {
destructive: true,
description: 'Delete a record by ID',
params: z.object({ id: z.string() }),
handler: async (ctx, args) => {
await ctx.prisma.user.delete({
where: { id: args.id, tenantId: ctx.tenantId },
});
return { deleted: true };
},
},
},
});Every query is tenant-isolated at the generated code level. Cross-tenant access is structurally impossible.
Wiring into Your Server
The generator produces ToolBuilder instances and Presenter files — no server, no transport. You import and wire them:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ToolRegistry, createServerAttachment } from '@vurb/core';
import { userTools } from './tools/database/userTools.js';
import { prisma } from './lib/prisma.js';
userTools.use(async (ctx, args, next) => {
if (!ctx.auth?.hasScope('users:read')) throw new Error('Unauthorized');
return next();
});
const registry = new ToolRegistry();
registry.register(userTools);
const server = new McpServer({ name: 'my-api', version: '1.0.0' });
createServerAttachment(server, registry, {
contextFactory: (req) => ({
prisma,
tenantId: extractTenantFromJWT(req),
auth: extractAuthFromJWT(req),
}),
});
await server.connect(new StdioServerTransport());Schema Annotations
| Annotation | Location | Effect |
|---|---|---|
/// @vurb.hide | Field | Excludes from the generated Zod response schema |
/// @vurb.describe("...") | Field | Adds .describe() to the Zod field |
/// @vurb.tenantKey | Field | Injects into every query's WHERE clause from ctx |
Generator Configuration
generator mcp {
provider = "vurb-prisma-gen"
output = "../src/tools/database"
}| Option | Type | Default | Description |
|---|---|---|---|
provider | string | — | Must be "Vurb.ts-prisma-gen" |
output | string | "./generated" | Output directory for generated files |
Generated Output
src/tools/database/
├── userPresenter.ts ← Zod schema + Presenter (fields filtered)
├── userTools.ts ← CRUD tool with pagination + tenant isolation
├── postPresenter.ts
├── postTools.ts
└── index.ts ← Barrel exportEach model produces a Presenter (Zod .strict() schema with @vurb.hide fields removed) and a Tool (defineTool() builder with find_many, find_unique, create, update, delete actions).
Requirements
| Dependency | Version |
|---|---|
| Node.js | ≥ 18 |
| Prisma | ≥ 5.0 |
Vurb.ts | ^2.0.0 (peer) |
zod | ^3.25.1 || ^4.0.0 (peer) |
@prisma/generator-helper | ^6.0.0 (peer) |