Payload CMS MCP Server

MIT License
322
7
  • Linux
  • Apple
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;`; }