Payload CMS MCP Server
- lib
- payload
import { z } from 'zod';
/**
* Types of templates that can be generated
*/
export type TemplateType =
| 'collection'
| 'field'
| 'global'
| 'config'
| 'access-control'
| 'hook'
| 'endpoint'
| 'plugin'
| 'block'
| 'migration';
/**
* Validation schema for template options
*/
const templateOptionsSchema = z.record(z.any());
/**
* Generate a template for Payload CMS 3 based on the template type and options
* @param templateType The type of template to generate
* @param options Options for the template
* @returns The generated code as a string
*/
export function generateTemplate(templateType: TemplateType, options: Record<string, any>): string {
// Validate options
const validationResult = templateOptionsSchema.safeParse(options);
if (!validationResult.success) {
throw new Error(`Invalid template options: ${JSON.stringify(validationResult.error.format())}`);
}
// Generate the template based on the type
switch (templateType) {
case 'collection':
return generateCollectionTemplate(options);
case 'field':
return generateFieldTemplate(options);
case 'global':
return generateGlobalTemplate(options);
case 'config':
return generateConfigTemplate(options);
case 'access-control':
return generateAccessControlTemplate(options);
case 'hook':
return generateHookTemplate(options);
case 'endpoint':
return generateEndpointTemplate(options);
case 'plugin':
return generatePluginTemplate(options);
case 'block':
return generateBlockTemplate(options);
case 'migration':
return generateMigrationTemplate(options);
default:
throw new Error(`Unsupported template type: ${templateType}`);
}
}
/**
* Generate a collection template for Payload CMS 3
* @param options Collection options
* @returns The generated collection code
*/
function generateCollectionTemplate(options: Record<string, any>): string {
const {
slug,
fields = [],
auth = false,
timestamps = true,
admin = {},
hooks = false,
access = false,
versions = false,
} = options;
if (!slug) {
throw new Error('Collection slug is required');
}
// Generate fields code
const fieldsCode = fields.length > 0
? fields.map((field: any) => {
return generateFieldTemplate(field);
}).join(',\n ')
: '';
// Generate admin code
const adminCode = Object.keys(admin).length > 0
? `\n admin: {
${admin.useAsTitle ? `useAsTitle: '${admin.useAsTitle}',` : ''}
${admin.defaultColumns ? `defaultColumns: [${admin.defaultColumns.map((col: string) => `'${col}'`).join(', ')}],` : ''}
${admin.group ? `group: '${admin.group}',` : ''}
},`
: '';
// Generate hooks code
const hooksCode = hooks
? `\n hooks: {
beforeOperation: [
// Add your hooks here
],
afterOperation: [
// Add your hooks here
],
},`
: '';
// Generate access code
const accessCode = access
? `\n access: {
read: () => true,
update: () => true,
create: () => true,
delete: () => true,
},`
: '';
// Generate auth code
const authCode = auth
? `\n auth: {
useAPIKey: true,
tokenExpiration: 7200,
},`
: '';
// Generate versions code
const versionsCode = versions
? `\n versions: {
drafts: true,
},`
: '';
return `import { CollectionConfig } from 'payload/types';
const ${slug.charAt(0).toUpperCase() + slug.slice(1)}: CollectionConfig = {
slug: '${slug}',${adminCode}${authCode}${accessCode}${hooksCode}${versionsCode}
${timestamps ? 'timestamps: true,' : ''}
fields: [
${fieldsCode}
],
};
export default ${slug.charAt(0).toUpperCase() + slug.slice(1)};`;
}
/**
* Generate a field template for Payload CMS 3
* @param options Field options
* @returns The generated field code
*/
function generateFieldTemplate(options: Record<string, any>): string {
const {
name,
type,
required = false,
unique = false,
localized = false,
access = false,
admin = {},
validation = false,
defaultValue,
} = options;
if (!name || !type) {
throw new Error('Field name and type are required');
}
// Generate admin code
const adminCode = Object.keys(admin).length > 0
? `\n admin: {
${admin.description ? `description: '${admin.description}',` : ''}
${admin.readOnly ? 'readOnly: true,' : ''}
},`
: '';
// Generate access code
const accessCode = access
? `\n access: {
read: () => true,
update: () => true,
},`
: '';
// Generate validation code
const validationCode = validation
? `\n validate: (value) => {
if (value === undefined || value === null) {
return '${name} is required';
}
return true;
},`
: '';
// Generate default value code
const defaultValueCode = defaultValue !== undefined
? `\n defaultValue: ${typeof defaultValue === 'string' ? `'${defaultValue}'` : defaultValue},`
: '';
// Generate field-specific options based on type
let fieldSpecificOptions = '';
switch (type) {
case 'text':
case 'textarea':
case 'email':
case 'code':
fieldSpecificOptions = `\n minLength: 1,
maxLength: 255,`;
break;
case 'number':
fieldSpecificOptions = `\n min: 0,
max: 1000,`;
break;
case 'select':
fieldSpecificOptions = `\n options: [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
],
hasMany: false,`;
break;
case 'relationship':
fieldSpecificOptions = `\n relationTo: 'collection-name',
hasMany: false,`;
break;
case 'array':
fieldSpecificOptions = `\n minRows: 0,
maxRows: 10,
fields: [
{
name: 'subField',
type: 'text',
required: true,
},
],`;
break;
case 'blocks':
fieldSpecificOptions = `\n blocks: [
{
slug: 'block-name',
fields: [
{
name: 'blockField',
type: 'text',
required: true,
},
],
},
],`;
break;
}
return `{
name: '${name}',
type: '${type}',${required ? '\n required: true,' : ''}${unique ? '\n unique: true,' : ''}${localized ? '\n localized: true,' : ''}${adminCode}${accessCode}${validationCode}${defaultValueCode}${fieldSpecificOptions}
}`;
}
/**
* Generate a global template for Payload CMS 3
* @param options Global options
* @returns The generated global code
*/
function generateGlobalTemplate(options: Record<string, any>): string {
const {
slug,
fields = [],
admin = {},
access = false,
versions = false,
} = options;
if (!slug) {
throw new Error('Global slug is required');
}
// Generate fields code
const fieldsCode = fields.length > 0
? fields.map((field: any) => {
return generateFieldTemplate(field);
}).join(',\n ')
: '';
// Generate admin code
const adminCode = Object.keys(admin).length > 0
? `\n admin: {
${admin.group ? `group: '${admin.group}',` : ''}
},`
: '';
// Generate access code
const accessCode = access
? `\n access: {
read: () => true,
update: () => true,
},`
: '';
// Generate versions code
const versionsCode = versions
? `\n versions: {
drafts: true,
},`
: '';
return `import { GlobalConfig } from 'payload/types';
const ${slug.charAt(0).toUpperCase() + slug.slice(1)}: GlobalConfig = {
slug: '${slug}',${adminCode}${accessCode}${versionsCode}
fields: [
${fieldsCode}
],
};
export default ${slug.charAt(0).toUpperCase() + slug.slice(1)};`;
}
/**
* Generate a config template for Payload CMS 3
* @param options Config options
* @returns The generated config code
*/
function generateConfigTemplate(options: Record<string, any>): string {
const {
serverURL = 'http://localhost:3000',
collections = [],
globals = [],
admin = {},
db = 'mongodb',
plugins = [],
typescript = true,
} = options;
// Generate collections code
const collectionsCode = collections.length > 0
? collections.map((collection: string) => `import ${collection.charAt(0).toUpperCase() + collection.slice(1)} from './collections/${collection}';`).join('\n')
: '';
// Generate globals code
const globalsCode = globals.length > 0
? globals.map((global: string) => `import ${global.charAt(0).toUpperCase() + global.slice(1)} from './globals/${global}';`).join('\n')
: '';
// Generate plugins code
const pluginsCode = plugins.length > 0
? plugins.map((plugin: string) => {
if (plugin === 'form-builder') {
return `import formBuilder from '@payloadcms/plugin-form-builder';`;
} else if (plugin === 'seo') {
return `import seoPlugin from '@payloadcms/plugin-seo';`;
} else if (plugin === 'nested-docs') {
return `import nestedDocs from '@payloadcms/plugin-nested-docs';`;
} else {
return `import ${plugin} from '@payloadcms/plugin-${plugin}';`;
}
}).join('\n')
: '';
// Generate plugins initialization code
const pluginsInitCode = plugins.length > 0
? `\n plugins: [
${plugins.map((plugin: string) => {
if (plugin === 'form-builder') {
return `formBuilder({
formOverrides: {
admin: {
group: 'Content',
},
},
formSubmissionOverrides: {
admin: {
group: 'Content',
},
},
redirectRelationships: ['pages'],
}),`;
} else if (plugin === 'seo') {
return `seoPlugin(),`;
} else if (plugin === 'nested-docs') {
return `nestedDocs({
collections: ['pages'],
}),`;
} else {
return `${plugin}(),`;
}
}).join('\n ')}
],`
: '';
// Generate admin code
const adminInitCode = Object.keys(admin).length > 0
? `\n admin: {
user: '${admin.user || 'users'}',
bundler: ${admin.bundler === 'vite' ? 'viteBundler()' : 'webpackBundler()'},
meta: {
titleSuffix: '- Payload CMS',
favicon: '/assets/favicon.ico',
ogImage: '/assets/og-image.jpg',
},
},`
: '';
// Generate database code
const dbCode = db === 'postgres'
? `\n db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI,
},
}),`
: `\n db: mongooseAdapter({
url: process.env.MONGODB_URI,
}),`;
// Generate collections and globals initialization
const collectionsInitCode = collections.length > 0
? `\n collections: [
${collections.map((collection: string) => `${collection.charAt(0).toUpperCase() + collection.slice(1)},`).join('\n ')}
],`
: '';
const globalsInitCode = globals.length > 0
? `\n globals: [
${globals.map((global: string) => `${global.charAt(0).toUpperCase() + global.slice(1)},`).join('\n ')}
],`
: '';
// Generate imports for database adapters
const dbImports = db === 'postgres'
? `import { postgresAdapter } from '@payloadcms/db-postgres';`
: `import { mongooseAdapter } from '@payloadcms/db-mongoose';`;
// Generate bundler imports
const bundlerImports = admin.bundler === 'vite'
? `import { viteBundler } from '@payloadcms/bundler-vite';`
: `import { webpackBundler } from '@payloadcms/bundler-webpack';`;
return `import path from 'path';
import { buildConfig } from 'payload/config';
${dbImports}
${bundlerImports}
${collectionsCode ? `\n${collectionsCode}` : ''}
${globalsCode ? `\n${globalsCode}` : ''}
${pluginsCode ? `\n${pluginsCode}` : ''}
export default buildConfig({
serverURL: '${serverURL}',${adminInitCode}${dbCode}${pluginsInitCode}${collectionsInitCode}${globalsInitCode}
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
},
graphQL: {
schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
},
cors: ['http://localhost:3000'],
csrf: [
'http://localhost:3000',
],
});`;
}
/**
* Generate an access control template for Payload CMS 3
* @param options Access control options
* @returns The generated access control code
*/
function generateAccessControlTemplate(options: Record<string, any>): string {
const {
type = 'collection',
name = 'default',
roles = ['admin', 'editor', 'user'],
} = options;
return `import { Access } from 'payload/types';
// Define user roles type
type Role = ${roles.map(role => `'${role}'`).join(' | ')};
// Access control for ${type} ${name}
export const ${name}Access: Access = ({ req }) => {
// If there's no user, deny access
if (!req.user) {
return false;
}
// Admin users can do anything
if (req.user.role === 'admin') {
return true;
}
// Editor users can read and update but not delete
if (req.user.role === 'editor') {
return {
read: true,
update: true,
create: true,
delete: false,
};
}
// Regular users can only read their own documents
if (req.user.role === 'user') {
return {
read: {
and: [
{
createdBy: {
equals: req.user.id,
},
},
],
},
update: {
createdBy: {
equals: req.user.id,
},
},
create: true,
delete: {
createdBy: {
equals: req.user.id,
},
},
};
}
// Default deny
return false;
};`;
}
/**
* Generate a hook template for Payload CMS 3
* @param options Hook options
* @returns The generated hook code
*/
function generateHookTemplate(options: Record<string, any>): string {
const {
type = 'collection',
name = 'default',
operation = 'create',
timing = 'before',
} = options;
return `import { ${timing === 'before' ? 'BeforeOperation' : 'AfterOperation'} } from 'payload/types';
// ${timing}${operation.charAt(0).toUpperCase() + operation.slice(1)} hook for ${type} ${name}
export const ${timing}${operation.charAt(0).toUpperCase() + operation.slice(1)}Hook: ${timing === 'before' ? 'BeforeOperation' : 'AfterOperation'} = async ({
req,
data,
operation,
${timing === 'after' ? 'doc,' : ''}
${timing === 'after' ? 'previousDoc,' : ''}
}) => {
// Your hook logic here
console.log(\`${timing} ${operation} operation on ${type} ${name}\`);
${timing === 'before'
? `// You can modify the data before it's saved
return data;`
: `// You can perform actions after the operation
return doc;`}
};`;
}
/**
* Generate an endpoint template for Payload CMS 3
* @param options Endpoint options
* @returns The generated endpoint code
*/
function generateEndpointTemplate(options: Record<string, any>): string {
const {
path = '/api/custom',
method = 'get',
auth = true,
} = options;
return `import { Payload } from 'payload';
import { Request, Response } from 'express';
// Custom endpoint handler
export const ${method}${path.replace(/\//g, '_').replace(/^_/, '').replace(/_$/, '')} = async (req: Request, res: Response, payload: Payload) => {
try {
${auth ? `// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
message: 'Unauthorized',
});
}` : ''}
// Your endpoint logic here
const result = {
message: 'Success',
timestamp: new Date().toISOString(),
};
// Return successful response
return res.status(200).json(result);
} catch (error) {
// Handle errors
console.error(\`Error in ${path} endpoint:\`, error);
return res.status(500).json({
message: 'Internal Server Error',
error: error.message,
});
}
};
// Endpoint configuration
export default {
path: '${path}',
method: '${method}',
handler: ${method}${path.replace(/\//g, '_').replace(/^_/, '').replace(/_$/, '')},
};`;
}
/**
* Generate a plugin template for Payload CMS 3
* @param options Plugin options
* @returns The generated plugin code
*/
function generatePluginTemplate(options: Record<string, any>): string {
const {
name = 'custom-plugin',
collections = [],
globals = [],
fields = [],
endpoints = [],
} = options;
return `import { Config, Plugin } from 'payload/config';
// Define the plugin options type
export interface ${name.replace(/-/g, '_').charAt(0).toUpperCase() + name.replace(/-/g, '_').slice(1)}PluginOptions {
// Add your plugin options here
enabled?: boolean;
}
// Define the plugin
export const ${name.replace(/-/g, '_')}Plugin = (options: ${name.replace(/-/g, '_').charAt(0).toUpperCase() + name.replace(/-/g, '_').slice(1)}PluginOptions = {}): Plugin => {
return {
// Plugin name
name: '${name}',
// Plugin configuration function
config: (incomingConfig: Config): Config => {
// Default options
const { enabled = true } = options;
if (!enabled) {
return incomingConfig;
}
// Create a new config to modify
const config = { ...incomingConfig };
// Add collections
${collections.length > 0 ? `
// Add plugin collections
const collections = [
// Define your collections here
${collections.map((collection: string) => `{
slug: '${collection}',
// Add collection configuration
}`).join(',\n ')}
];
config.collections = [
...(config.collections || []),
...collections,
];` : '// No collections to add'}
// Add globals
${globals.length > 0 ? `
// Add plugin globals
const globals = [
// Define your globals here
${globals.map((global: string) => `{
slug: '${global}',
// Add global configuration
}`).join(',\n ')}
];
config.globals = [
...(config.globals || []),
...globals,
];` : '// No globals to add'}
// Add endpoints
${endpoints.length > 0 ? `
// Add plugin endpoints
const endpoints = [
// Define your endpoints here
${endpoints.map((endpoint: string) => `{
path: '/${endpoint}',
method: 'get',
handler: async (req, res) => {
res.status(200).json({ message: '${endpoint} endpoint' });
},
}`).join(',\n ')}
];
config.endpoints = [
...(config.endpoints || []),
...endpoints,
];` : '// No endpoints to add'}
// Return the modified config
return config;
},
};
};
export default ${name.replace(/-/g, '_')}Plugin;`;
}
/**
* Generate a block template for Payload CMS 3
* @param options Block options
* @returns The generated block code
*/
function generateBlockTemplate(options: Record<string, any>): string {
const {
name = 'custom-block',
fields = [],
imageField = true,
contentField = true,
} = options;
// Generate fields code
const fieldsCode = fields.length > 0
? fields.map((field: any) => {
return generateFieldTemplate(field);
}).join(',\n ')
: '';
// Generate image field
const imageFieldCode = imageField
? `{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
admin: {
description: 'Add an image to this block',
},
},`
: '';
// Generate content field
const contentFieldCode = contentField
? `{
name: 'content',
type: 'richText',
required: true,
admin: {
description: 'Add content to this block',
},
},`
: '';
return `import { Block } from 'payload/types';
// Define the ${name} block
export const ${name.replace(/-/g, '_')}Block: Block = {
slug: '${name}',
labels: {
singular: '${name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ')}',
plural: '${name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ')}s',
},
fields: [
${imageFieldCode}
${contentFieldCode}
${fieldsCode}
],
};
export default ${name.replace(/-/g, '_')}Block;`;
}
/**
* Generate a migration template for Payload CMS 3
* @param options Migration options
* @returns The generated migration code
*/
function generateMigrationTemplate(options: Record<string, any>): string {
const {
name = 'custom-migration',
collection = '',
operation = 'update',
} = options;
return `import { Payload } from 'payload';
// Migration: ${name}
export const ${name.replace(/-/g, '_')}Migration = async (payload: Payload) => {
try {
console.log('Starting migration: ${name}');
${collection ? `// Get the collection
const collection = '${collection}';
// Find documents to migrate
const docs = await payload.find({
collection,
limit: 100,
});
console.log(\`Found \${docs.docs.length} documents to migrate\`);
// Process each document
for (const doc of docs.docs) {
${operation === 'update' ? `// Update the document
await payload.update({
collection,
id: doc.id,
data: {
// Add your migration changes here
migratedAt: new Date().toISOString(),
},
});` : operation === 'delete' ? `// Delete the document
await payload.delete({
collection,
id: doc.id,
});` : `// Custom operation
// Add your custom migration logic here`}
}` : `// Add your migration logic here
// This could be schema changes, data transformations, etc.`}
console.log('Migration completed successfully: ${name}');
return { success: true };
} catch (error) {
console.error('Migration failed:', error);
return { success: false, error: error.message };
}
};
export default ${name.replace(/-/g, '_')}Migration;`;
}