@govcraft/payload-cms-mcp 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +164 -0
  2. package/dist/config/index.d.ts +27 -0
  3. package/dist/config/index.js +31 -0
  4. package/dist/config/index.js.map +1 -0
  5. package/dist/controllers/mcp.controller.d.ts +7 -0
  6. package/dist/controllers/mcp.controller.js +328 -0
  7. package/dist/controllers/mcp.controller.js.map +1 -0
  8. package/dist/controllers/payload-mcp.controller.d.ts +4 -0
  9. package/dist/controllers/payload-mcp.controller.js +185 -0
  10. package/dist/controllers/payload-mcp.controller.js.map +1 -0
  11. package/dist/generate-tools.d.ts +1 -0
  12. package/dist/generate-tools.js +1417 -0
  13. package/dist/generate-tools.js.map +1 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +16 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/mcp/anthropic-mcp.d.ts +3 -0
  18. package/dist/mcp/anthropic-mcp.js +332 -0
  19. package/dist/mcp/anthropic-mcp.js.map +1 -0
  20. package/dist/mcp/generated/index.d.ts +44 -0
  21. package/dist/mcp/generated/index.js +45 -0
  22. package/dist/mcp/generated/index.js.map +1 -0
  23. package/dist/mcp/generated/payload-tools.d.ts +7 -0
  24. package/dist/mcp/generated/payload-tools.js +69 -0
  25. package/dist/mcp/generated/payload-tools.js.map +1 -0
  26. package/dist/mcp/generated/payload-tools.json +13024 -0
  27. package/dist/mcp/generated/tools/create.json +9138 -0
  28. package/dist/mcp/generated/tools/createAccess.json +10 -0
  29. package/dist/mcp/generated/tools/createAfterChangeHook.json +9 -0
  30. package/dist/mcp/generated/tools/createAfterDeleteHook.json +9 -0
  31. package/dist/mcp/generated/tools/createAfterErrorHook.json +9 -0
  32. package/dist/mcp/generated/tools/createAfterForgotPasswordHook.json +9 -0
  33. package/dist/mcp/generated/tools/createAfterLoginHook.json +9 -0
  34. package/dist/mcp/generated/tools/createAfterLogoutHook.json +9 -0
  35. package/dist/mcp/generated/tools/createAfterMeHook.json +9 -0
  36. package/dist/mcp/generated/tools/createAfterOperationHook.json +9 -0
  37. package/dist/mcp/generated/tools/createAfterReadHook.json +9 -0
  38. package/dist/mcp/generated/tools/createAfterRefreshHook.json +9 -0
  39. package/dist/mcp/generated/tools/createArrayField.json +90 -0
  40. package/dist/mcp/generated/tools/createBeforeChangeHook.json +9 -0
  41. package/dist/mcp/generated/tools/createBeforeDeleteHook.json +9 -0
  42. package/dist/mcp/generated/tools/createBeforeLoginHook.json +9 -0
  43. package/dist/mcp/generated/tools/createBeforeOperationHook.json +9 -0
  44. package/dist/mcp/generated/tools/createBeforeReadHook.json +9 -0
  45. package/dist/mcp/generated/tools/createBeforeValidateHook.json +9 -0
  46. package/dist/mcp/generated/tools/createBlocksField.json +79 -0
  47. package/dist/mcp/generated/tools/createCodeField.json +79 -0
  48. package/dist/mcp/generated/tools/createCollection.json +422 -0
  49. package/dist/mcp/generated/tools/createCollectionAdminOptions.json +2789 -0
  50. package/dist/mcp/generated/tools/createDateField.json +84 -0
  51. package/dist/mcp/generated/tools/createEmailField.json +87 -0
  52. package/dist/mcp/generated/tools/createField.json +31 -0
  53. package/dist/mcp/generated/tools/createGlobal.json +220 -0
  54. package/dist/mcp/generated/tools/createGroupField.json +79 -0
  55. package/dist/mcp/generated/tools/createJSONField.json +79 -0
  56. package/dist/mcp/generated/tools/createJoinField.json +79 -0
  57. package/dist/mcp/generated/tools/createMeHook.json +9 -0
  58. package/dist/mcp/generated/tools/createNumberField.json +31 -0
  59. package/dist/mcp/generated/tools/createPointField.json +79 -0
  60. package/dist/mcp/generated/tools/createPolymorphicRelationshipField.json +31 -0
  61. package/dist/mcp/generated/tools/createRadioField.json +94 -0
  62. package/dist/mcp/generated/tools/createRefreshHook.json +9 -0
  63. package/dist/mcp/generated/tools/createRelationshipField.json +31 -0
  64. package/dist/mcp/generated/tools/createRichTextField.json +79 -0
  65. package/dist/mcp/generated/tools/createSelectField.json +31 -0
  66. package/dist/mcp/generated/tools/createSingleRelationshipField.json +31 -0
  67. package/dist/mcp/generated/tools/createTextField.json +31 -0
  68. package/dist/mcp/generated/tools/createTextareaField.json +87 -0
  69. package/dist/mcp/generated/tools/createUploadField.json +31 -0
  70. package/dist/mcp/handler.d.ts +6 -0
  71. package/dist/mcp/handler.js +147 -0
  72. package/dist/mcp/handler.js.map +1 -0
  73. package/dist/mcp/index.d.ts +1 -0
  74. package/dist/mcp/index.js +28 -0
  75. package/dist/mcp/index.js.map +1 -0
  76. package/dist/mcp/io/index.d.ts +2 -0
  77. package/dist/mcp/io/index.js +31 -0
  78. package/dist/mcp/io/index.js.map +1 -0
  79. package/dist/mcp/io/stdin.d.ts +2 -0
  80. package/dist/mcp/io/stdin.js +118 -0
  81. package/dist/mcp/io/stdin.js.map +1 -0
  82. package/dist/mcp/session.d.ts +16 -0
  83. package/dist/mcp/session.js +85 -0
  84. package/dist/mcp/session.js.map +1 -0
  85. package/dist/mcp/sse.d.ts +10 -0
  86. package/dist/mcp/sse.js +86 -0
  87. package/dist/mcp/sse.js.map +1 -0
  88. package/dist/mcp/tools/calculator.d.ts +2 -0
  89. package/dist/mcp/tools/calculator.js +68 -0
  90. package/dist/mcp/tools/calculator.js.map +1 -0
  91. package/dist/mcp/tools/index.d.ts +3 -0
  92. package/dist/mcp/tools/index.js +6 -0
  93. package/dist/mcp/tools/index.js.map +1 -0
  94. package/dist/mcp/tools/list-tools.d.ts +2 -0
  95. package/dist/mcp/tools/list-tools.js +47 -0
  96. package/dist/mcp/tools/list-tools.js.map +1 -0
  97. package/dist/mcp/tools/payload-tools-loader.d.ts +2 -0
  98. package/dist/mcp/tools/payload-tools-loader.js +21 -0
  99. package/dist/mcp/tools/payload-tools-loader.js.map +1 -0
  100. package/dist/mcp/types.d.ts +69 -0
  101. package/dist/mcp/types.js +2 -0
  102. package/dist/mcp/types.js.map +1 -0
  103. package/dist/middleware/errorHandler.d.ts +14 -0
  104. package/dist/middleware/errorHandler.js +25 -0
  105. package/dist/middleware/errorHandler.js.map +1 -0
  106. package/dist/middleware/notFoundHandler.d.ts +2 -0
  107. package/dist/middleware/notFoundHandler.js +8 -0
  108. package/dist/middleware/notFoundHandler.js.map +1 -0
  109. package/dist/routes/health.d.ts +2 -0
  110. package/dist/routes/health.js +18 -0
  111. package/dist/routes/health.js.map +1 -0
  112. package/dist/routes/mcp.d.ts +2 -0
  113. package/dist/routes/mcp.js +11 -0
  114. package/dist/routes/mcp.js.map +1 -0
  115. package/dist/routes/payload-mcp.routes.d.ts +3 -0
  116. package/dist/routes/payload-mcp.routes.js +8 -0
  117. package/dist/routes/payload-mcp.routes.js.map +1 -0
  118. package/dist/utils/logger.d.ts +2 -0
  119. package/dist/utils/logger.js +35 -0
  120. package/dist/utils/logger.js.map +1 -0
  121. package/package.json +64 -0
@@ -0,0 +1,1417 @@
1
+ import { Project } from 'ts-morph';
2
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { logger } from './utils/logger.js';
5
+ const OUTPUT_DIR = path.join(process.cwd(), 'src', 'mcp', 'generated');
6
+ if (!existsSync(OUTPUT_DIR))
7
+ mkdirSync(OUTPUT_DIR, { recursive: true });
8
+ function extractJSDocDescription(node) {
9
+ const jsDocs = node.getJsDocs();
10
+ if (jsDocs.length === 0)
11
+ return undefined;
12
+ const combinedDescription = jsDocs.map(doc => {
13
+ const description = doc.getDescription().trim();
14
+ const tags = doc.getTags().map(tag => {
15
+ const tagName = tag.getTagName();
16
+ const comment = tag.getComment() || '';
17
+ return `@${tagName} ${comment}`.trim();
18
+ }).join('\n');
19
+ return [description, tags].filter(Boolean).join('\n');
20
+ }).join('\n\n');
21
+ return combinedDescription || undefined;
22
+ }
23
+ function extractPropertyJSDoc(prop) {
24
+ return extractJSDocDescription(prop);
25
+ }
26
+ function enhanceSchemaWithJSDocs(schema, _type, decl) {
27
+ if (!schema || schema.type !== 'object' || !schema.properties)
28
+ return schema;
29
+ if (decl) {
30
+ const typeDescription = extractJSDocDescription(decl);
31
+ if (typeDescription) {
32
+ schema.description = typeDescription;
33
+ }
34
+ }
35
+ if (decl && 'getProperties' in decl) {
36
+ const properties = decl.getProperties();
37
+ for (const prop of properties) {
38
+ const propName = prop.getName();
39
+ if (schema.properties[propName]) {
40
+ const propJSDoc = extractPropertyJSDoc(prop);
41
+ if (propJSDoc) {
42
+ schema.properties[propName].description = propJSDoc;
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return schema;
48
+ }
49
+ function cleanTypeName(typeName) {
50
+ let cleaned = typeName.replace(/import\(".*?\/node_modules\/payload\/dist\/(.*?)"\)\./, 'Payload.');
51
+ cleaned = cleaned.replace(/<.*?>/, '');
52
+ cleaned = cleaned.replace(/import\(".*?"\)\./g, '');
53
+ return cleaned;
54
+ }
55
+ function typeToJsonSchema(type, _decl) {
56
+ if (type.isString())
57
+ return { type: 'string' };
58
+ if (type.isNumber())
59
+ return { type: 'number' };
60
+ if (type.isBoolean())
61
+ return { type: 'boolean' };
62
+ if (type.isArray()) {
63
+ const elementType = type.getArrayElementType();
64
+ if (elementType && elementType.isUnion() && elementType.getUnionTypes().some(t => t.getProperties().some(p => p.getName() === 'type'))) {
65
+ return {
66
+ type: 'array',
67
+ items: {
68
+ type: 'object',
69
+ properties: {
70
+ name: { type: 'string', description: 'The name of the field' },
71
+ type: { type: 'string', description: 'The type of field' },
72
+ label: { type: 'string', description: 'The label shown in the admin UI' },
73
+ required: { type: 'boolean', description: 'Whether this field is required' },
74
+ admin: {
75
+ type: 'object',
76
+ description: 'Admin panel configuration for this field',
77
+ properties: {
78
+ description: { type: 'string', description: 'Additional description shown in the admin UI' },
79
+ position: { type: 'string', enum: ['sidebar'], description: 'Position of the field in the admin UI' },
80
+ width: { type: 'string', description: 'Width of the field in the admin UI' },
81
+ style: { type: 'object', description: 'Custom CSS styles for the field' },
82
+ className: { type: 'string', description: 'CSS class name for the field' },
83
+ readOnly: { type: 'boolean', description: 'Whether the field is read-only in the admin UI' },
84
+ hidden: { type: 'boolean', description: 'Whether the field is hidden in the admin UI' },
85
+ condition: { type: 'string', description: 'Condition for showing/hiding the field' },
86
+ components: {
87
+ type: 'object',
88
+ description: 'Custom components for the field',
89
+ properties: {
90
+ Field: { type: 'string', description: 'Custom field component' },
91
+ Cell: { type: 'string', description: 'Custom cell component for list views' },
92
+ Filter: { type: 'string', description: 'Custom filter component' }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ },
98
+ required: ['name', 'type']
99
+ },
100
+ description: 'Array of field configurations'
101
+ };
102
+ }
103
+ if (elementType && elementType.isObject() &&
104
+ elementType.getProperties().some(p => p.getName() === 'label') &&
105
+ elementType.getProperties().some(p => p.getName() === 'value')) {
106
+ return {
107
+ type: 'array',
108
+ items: {
109
+ type: 'object',
110
+ properties: {
111
+ label: { type: 'string', description: 'Display label for the option' },
112
+ value: {
113
+ oneOf: [
114
+ { type: 'string', description: 'Value stored in the database' },
115
+ { type: 'number', description: 'Numeric value stored in the database' }
116
+ ]
117
+ }
118
+ },
119
+ required: ['label', 'value']
120
+ },
121
+ description: 'Array of options for select/radio fields'
122
+ };
123
+ }
124
+ return {
125
+ type: 'array',
126
+ items: elementType ? typeToJsonSchema(elementType) : { type: 'object', description: 'Generic object' }
127
+ };
128
+ }
129
+ if (type.isObject()) {
130
+ const properties = {};
131
+ const required = [];
132
+ type.getProperties().forEach((prop) => {
133
+ const propName = prop.getName();
134
+ try {
135
+ const declarations = prop.getDeclarations();
136
+ const propType = declarations.length > 0 ? prop.getTypeAtLocation(declarations[0]) : prop.getType();
137
+ if (propType.getCallSignatures().length > 0) {
138
+ properties[propName] = {
139
+ type: 'string',
140
+ description: 'Function expression (e.g., "({ req }) => req.user.role === \'admin\'")'
141
+ };
142
+ return;
143
+ }
144
+ let propDescription;
145
+ if (declarations.length > 0 && declarations[0].getKind() === 166) {
146
+ propDescription = extractPropertyJSDoc(declarations[0]);
147
+ }
148
+ properties[propName] = typeToJsonSchema(propType);
149
+ if (propDescription) {
150
+ properties[propName].description = propDescription;
151
+ }
152
+ if (!prop.isOptional())
153
+ required.push(propName);
154
+ }
155
+ catch (error) {
156
+ logger.warn(`Failed to process property ${propName} in type ${type.getText()}: ${error instanceof Error ? error.message : String(error)}`);
157
+ if (propName.includes('path') || propName.includes('url') || propName.includes('name') ||
158
+ propName.includes('label') || propName.includes('title') || propName.includes('description')) {
159
+ properties[propName] = { type: 'string', description: `${propName} string value` };
160
+ }
161
+ else if (propName.includes('count') || propName.includes('limit') || propName.includes('max') ||
162
+ propName.includes('min') || propName.includes('size') || propName.includes('length')) {
163
+ properties[propName] = { type: 'number', description: `${propName} numeric value` };
164
+ }
165
+ else if (propName.includes('enabled') || propName.includes('disabled') || propName.includes('required') ||
166
+ propName.includes('visible') || propName.includes('hidden') || propName.startsWith('is') ||
167
+ propName.startsWith('has') || propName.startsWith('can')) {
168
+ properties[propName] = { type: 'boolean', description: `${propName} boolean flag` };
169
+ }
170
+ else if (propName.includes('options') || propName.includes('items') || propName.includes('list')) {
171
+ properties[propName] = {
172
+ type: 'array',
173
+ items: { type: 'object' },
174
+ description: `${propName} array of items`
175
+ };
176
+ }
177
+ else {
178
+ properties[propName] = {
179
+ type: 'object',
180
+ description: `${propName} configuration object`
181
+ };
182
+ }
183
+ }
184
+ });
185
+ return {
186
+ type: 'object',
187
+ properties,
188
+ required: required.length ? required : undefined
189
+ };
190
+ }
191
+ if (type.isUnion()) {
192
+ const unionTypes = type.getUnionTypes();
193
+ const isFieldUnion = unionTypes.some(t => {
194
+ const props = t.getProperties();
195
+ return props.some(p => p.getName() === 'type') &&
196
+ props.some(p => p.getName() === 'name');
197
+ });
198
+ if (isFieldUnion) {
199
+ return {
200
+ type: 'object',
201
+ properties: {
202
+ name: { type: 'string', description: 'The name of the field' },
203
+ type: { type: 'string', description: 'The type of field' },
204
+ label: { type: 'string', description: 'The label shown in the admin UI' },
205
+ required: { type: 'boolean', description: 'Whether this field is required' }
206
+ },
207
+ required: ['name', 'type'],
208
+ description: 'Field configuration'
209
+ };
210
+ }
211
+ const stringLiterals = unionTypes.filter(t => t.isStringLiteral());
212
+ if (stringLiterals.length === unionTypes.length) {
213
+ return {
214
+ type: 'string',
215
+ enum: stringLiterals.map(t => t.getLiteralValueOrThrow()),
216
+ description: 'One of the allowed string values'
217
+ };
218
+ }
219
+ const numberLiterals = unionTypes.filter(t => t.isNumberLiteral());
220
+ if (numberLiterals.length === unionTypes.length) {
221
+ return {
222
+ type: 'number',
223
+ enum: numberLiterals.map(t => t.getLiteralValueOrThrow()),
224
+ description: 'One of the allowed numeric values'
225
+ };
226
+ }
227
+ const booleanTypes = unionTypes.filter(t => t.isBoolean());
228
+ if (booleanTypes.length > 0) {
229
+ return { type: 'boolean', description: 'Boolean value' };
230
+ }
231
+ const stringTypes = unionTypes.filter(t => t.isString());
232
+ const numberTypes = unionTypes.filter(t => t.isNumber());
233
+ if (stringTypes.length > 0 && numberTypes.length > 0 &&
234
+ stringTypes.length + numberTypes.length === unionTypes.length) {
235
+ return {
236
+ oneOf: [
237
+ { type: 'string', description: 'String value' },
238
+ { type: 'number', description: 'Numeric value' }
239
+ ],
240
+ description: 'String or number value'
241
+ };
242
+ }
243
+ const nonNullTypes = unionTypes.filter(t => !t.isNull() && !t.isUndefined());
244
+ if (nonNullTypes.length === 1) {
245
+ return typeToJsonSchema(nonNullTypes[0]);
246
+ }
247
+ return {
248
+ oneOf: unionTypes
249
+ .filter(t => !t.isNull() && !t.isUndefined())
250
+ .map((t) => typeToJsonSchema(t)),
251
+ description: 'One of the allowed types'
252
+ };
253
+ }
254
+ if (type.isEnum()) {
255
+ const enumMembers = type.getSymbol()?.getDeclarations()?.[0]?.getType().getUnionTypes() || [];
256
+ const enumValues = enumMembers.map((t) => t.getLiteralValueOrThrow());
257
+ return {
258
+ type: 'string',
259
+ enum: enumValues,
260
+ description: 'One of the enum values'
261
+ };
262
+ }
263
+ const typeName = type.getText();
264
+ const cleanedTypeName = cleanTypeName(typeName);
265
+ if (typeName.includes('string') || typeName.includes('String')) {
266
+ return { type: 'string', description: 'String value' };
267
+ }
268
+ else if (typeName.includes('number') || typeName.includes('Number') ||
269
+ typeName.includes('int') || typeName.includes('float')) {
270
+ return { type: 'number', description: 'Numeric value' };
271
+ }
272
+ else if (typeName.includes('boolean') || typeName.includes('Boolean')) {
273
+ return { type: 'boolean', description: 'Boolean value' };
274
+ }
275
+ else if (typeName.includes('[]') || typeName.includes('Array')) {
276
+ return { type: 'array', items: { type: 'object' }, description: 'Array of items' };
277
+ }
278
+ else if (typeName.includes('Record') || typeName.includes('Map') || typeName.includes('Object')) {
279
+ return { type: 'object', properties: {}, description: 'Object with properties' };
280
+ }
281
+ else if (typeName.includes('Function') || typeName.includes('Callback')) {
282
+ return { type: 'string', description: 'Function expression' };
283
+ }
284
+ else if (typeName.includes('Date')) {
285
+ return { type: 'string', format: 'date-time', description: 'Date string (ISO format)' };
286
+ }
287
+ return {
288
+ type: 'object',
289
+ description: `${cleanedTypeName} configuration object`
290
+ };
291
+ }
292
+ function generateToolName(name) {
293
+ return `create${name.endsWith('Field') || name.endsWith('Hook') ? name : name.replace(/Config$/, '')}`;
294
+ }
295
+ async function generatePayloadTools() {
296
+ logger.info('Generating Payload CMS tools...');
297
+ const project = new Project();
298
+ const sourceFiles = project.addSourceFilesAtPaths([
299
+ 'node_modules/payload/dist/**/*.d.ts',
300
+ 'node_modules/payload/types/**/*.d.ts',
301
+ 'node_modules/payload/dist/types.d.ts',
302
+ 'node_modules/payload/dist/index.d.ts',
303
+ ]);
304
+ project.resolveSourceFileDependencies();
305
+ if (sourceFiles.length === 0) {
306
+ throw new Error('No Payload type definitions found. Run `pnpm install payload` first.');
307
+ }
308
+ logger.info(`Loaded ${sourceFiles.length} source files`);
309
+ const allInterfaces = sourceFiles.flatMap(file => file.getInterfaces());
310
+ const allTypeAliases = sourceFiles.flatMap(file => file.getTypeAliases());
311
+ const tools = [];
312
+ const targetNames = [
313
+ 'CollectionConfig', 'GlobalConfig', 'Config', 'SanitizedConfig', 'PayloadConfig',
314
+ 'Access', 'CollectionAdminOptions',
315
+ 'BeforeChangeHook', 'CollectionBeforeChangeHook', 'BeforeDeleteHook', 'CollectionBeforeDeleteHook',
316
+ 'BeforeLoginHook', 'CollectionBeforeLoginHook', 'BeforeOperationHook', 'CollectionBeforeOperationHook',
317
+ 'BeforeReadHook', 'CollectionBeforeReadHook', 'BeforeValidateHook', 'CollectionBeforeValidateHook',
318
+ 'AfterChangeHook', 'CollectionAfterChangeHook', 'AfterDeleteHook', 'CollectionAfterDeleteHook',
319
+ 'AfterErrorHook', 'CollectionAfterErrorHook', 'AfterForgotPasswordHook', 'CollectionAfterForgotPasswordHook',
320
+ 'AfterLoginHook', 'CollectionAfterLoginHook', 'AfterLogoutHook', 'CollectionAfterLogoutHook',
321
+ 'AfterMeHook', 'CollectionAfterMeHook', 'AfterOperationHook', 'CollectionAfterOperationHook',
322
+ 'AfterReadHook', 'CollectionAfterReadHook', 'AfterRefreshHook', 'CollectionAfterRefreshHook',
323
+ 'MeHook', 'CollectionMeHook', 'RefreshHook', 'CollectionRefreshHook',
324
+ 'Field', 'TextField', 'NumberField', 'DateField', 'EmailField', 'TextareaField',
325
+ 'RelationshipField', 'PolymorphicRelationshipField', 'SingleRelationshipField',
326
+ 'ArrayField', 'RichTextField', 'CodeField', 'JSONField', 'SelectField', 'RadioField',
327
+ 'PointField', 'BlocksField', 'JoinField', 'UploadField', 'GroupField'
328
+ ];
329
+ const processedNames = new Set();
330
+ const TOOLS_DIR = path.join(OUTPUT_DIR, 'tools');
331
+ if (!existsSync(TOOLS_DIR))
332
+ mkdirSync(TOOLS_DIR, { recursive: true });
333
+ for (const iface of allInterfaces) {
334
+ const name = iface.getName();
335
+ const baseName = name.replace('Sanitized', '');
336
+ if (targetNames.includes(name) || (name.startsWith('Sanitized') && targetNames.includes(baseName))) {
337
+ if (!processedNames.has(baseName)) {
338
+ logger.info(`Processing interface: ${name} from ${iface.getSourceFile().getFilePath()}`);
339
+ addTool(iface, name, tools);
340
+ processedNames.add(baseName);
341
+ }
342
+ else {
343
+ logger.debug(`Skipping duplicate interface: ${name} (base: ${baseName})`);
344
+ }
345
+ }
346
+ else {
347
+ logger.debug(`Skipping interface: ${name}`);
348
+ }
349
+ }
350
+ for (const typeAlias of allTypeAliases) {
351
+ const name = typeAlias.getName();
352
+ const baseName = name.replace('Sanitized', '');
353
+ if (targetNames.includes(name) || (name.startsWith('Sanitized') && targetNames.includes(baseName))) {
354
+ if (!processedNames.has(baseName)) {
355
+ logger.info(`Processing type alias: ${name} from ${typeAlias.getSourceFile().getFilePath()}`);
356
+ addTool(typeAlias, name, tools);
357
+ processedNames.add(baseName);
358
+ }
359
+ else {
360
+ logger.debug(`Skipping duplicate type alias: ${name} (base: ${baseName})`);
361
+ }
362
+ }
363
+ else {
364
+ logger.debug(`Skipping type alias: ${name}`);
365
+ }
366
+ }
367
+ const missingTargets = targetNames.filter(name => !processedNames.has(name) && !processedNames.has(name.replace('Sanitized', '')));
368
+ if (missingTargets.length > 0) {
369
+ logger.warn('Target names not found:', missingTargets);
370
+ }
371
+ logger.info('Generated tools:', tools.map(t => t.name));
372
+ for (const tool of tools) {
373
+ const toolFilePath = path.join(TOOLS_DIR, `${tool.name}.json`);
374
+ writeFileSync(toolFilePath, JSON.stringify(tool, null, 2));
375
+ logger.info(`Generated ${tool.name}.json`);
376
+ }
377
+ const jsContent = `
378
+ /**
379
+ * Auto-generated Payload CMS tools for MCP
380
+ * DO NOT EDIT DIRECTLY - Generated by generate-tools.ts
381
+ */
382
+ import { logger } from '../../utils/logger.js';
383
+ import fs from 'fs';
384
+ import path from 'path';
385
+ import { fileURLToPath } from 'url';
386
+
387
+ // Get the directory name of the current module
388
+ const __filename = fileURLToPath(import.meta.url);
389
+ const __dirname = path.dirname(__filename);
390
+
391
+ // Read all tool files from the tools directory
392
+ const toolsDir = path.join(__dirname, 'tools');
393
+ logger.info(\`Loading tools from directory: \${toolsDir}\`);
394
+
395
+ let payloadTools = [];
396
+ let toolsMap = {};
397
+
398
+ try {
399
+ // Check if the directory exists
400
+ if (!fs.existsSync(toolsDir)) {
401
+ logger.error(\`Tools directory does not exist: \${toolsDir}\`);
402
+ throw new Error(\`Tools directory does not exist: \${toolsDir}\`);
403
+ }
404
+
405
+ // List all files in the directory
406
+ const allFiles = fs.readdirSync(toolsDir);
407
+ logger.info(\`Found \${allFiles.length} files in tools directory\`);
408
+
409
+ // Filter for JSON files
410
+ const toolFiles = allFiles.filter(file => file.endsWith('.json'));
411
+ logger.info(\`Found \${toolFiles.length} JSON files in tools directory\`);
412
+
413
+ // Load each tool file
414
+ payloadTools = toolFiles.map(file => {
415
+ try {
416
+ const filePath = path.join(toolsDir, file);
417
+ logger.info(\`Loading tool file: \${filePath}\`);
418
+ const fileContent = fs.readFileSync(filePath, 'utf8');
419
+ const tool = JSON.parse(fileContent);
420
+ return {
421
+ name: tool.name,
422
+ description: tool.description,
423
+ parameters: tool.inputSchema,
424
+ template: tool.template
425
+ };
426
+ } catch (error) {
427
+ logger.error(\`Error loading tool file \${file}:\`, error);
428
+ return null;
429
+ }
430
+ }).filter(Boolean); // Remove any null entries from failed loads
431
+
432
+ logger.info(\`Successfully loaded \${payloadTools.length} tools\`);
433
+
434
+ // For backward compatibility
435
+ toolsMap = Object.fromEntries(
436
+ payloadTools.map(tool => [tool.name, tool])
437
+ );
438
+ } catch (error) {
439
+ logger.error(\`Error loading tools:\`, error);
440
+ // Provide empty arrays as fallbacks
441
+ payloadTools = [];
442
+ toolsMap = {};
443
+ }
444
+
445
+ export { payloadTools, toolsMap };
446
+ `;
447
+ writeFileSync(path.join(OUTPUT_DIR, 'payload-tools.js'), jsContent);
448
+ logger.info('Generated payload-tools.js');
449
+ const dtsContent = `
450
+ /**
451
+ * Auto-generated Payload CMS tools for MCP
452
+ */
453
+
454
+ /**
455
+ * Tool interface matching the SDK requirements
456
+ */
457
+ export interface PayloadTool {
458
+ name: string;
459
+ description: string;
460
+ parameters: {
461
+ type: string;
462
+ properties: Record<string, unknown>;
463
+ required?: string[];
464
+ };
465
+ template: string;
466
+ }
467
+
468
+ export const payloadTools: PayloadTool[];
469
+
470
+ /**
471
+ * Map of tool names to tool definitions
472
+ */
473
+ export interface PayloadToolsMap {
474
+ tools: Record<string, PayloadTool>;
475
+ }
476
+
477
+ /**
478
+ * For backward compatibility
479
+ */
480
+ export const toolsMap: Record<string, PayloadTool>;
481
+ `;
482
+ writeFileSync(path.join(OUTPUT_DIR, 'payload-tools.d.ts'), dtsContent);
483
+ logger.info('Generated payload-tools.d.ts');
484
+ const indexTsContent = `
485
+ /**
486
+ * Auto-generated Payload CMS tools for MCP
487
+ * DO NOT EDIT DIRECTLY - Generated by generate-tools.ts
488
+ */
489
+
490
+ // Export all tools
491
+ export * from './payload-tools.js';
492
+
493
+ // Export individual tools for direct imports
494
+ ${tools.map(tool => `export { default as ${tool.name} } from './tools/${tool.name}.json' with { type: 'json' };`).join('\n')}
495
+ `;
496
+ writeFileSync(path.join(OUTPUT_DIR, 'index.ts'), indexTsContent);
497
+ logger.info('Generated index.ts');
498
+ }
499
+ function addTool(decl, name, toolsArray) {
500
+ const jsDocDescription = extractJSDocDescription(decl);
501
+ const schema = enhanceSchemaWithJSDocs(typeToJsonSchema(decl.getType(), decl), decl.getType(), decl);
502
+ let template = '';
503
+ const baseName = name.replace('Sanitized', '');
504
+ const toolName = generateToolName(baseName);
505
+ if (baseName === 'CollectionConfig') {
506
+ template = `// Collection configuration for Payload CMS
507
+ export const {slug} = {
508
+ slug: '{slug}', // URL-friendly identifier for this collection
509
+ admin: {
510
+ useAsTitle: 'title', // Field to use as the title in the admin UI
511
+ defaultColumns: ['title', 'createdAt'], // Default columns in the admin UI list view
512
+ },
513
+ // Define the fields for this collection
514
+ fields: [
515
+ {
516
+ name: 'title',
517
+ type: 'text',
518
+ required: true,
519
+ },
520
+ // Add more fields as needed
521
+ {fields}
522
+ ],
523
+ // Optional: Add hooks, access control, etc.
524
+ ...{rest}
525
+ };`;
526
+ }
527
+ else if (baseName === 'GlobalConfig') {
528
+ template = `// Global configuration for Payload CMS
529
+ export const {slug} = {
530
+ slug: '{slug}', // URL-friendly identifier for this global
531
+ admin: {
532
+ group: 'Settings', // Group in the admin UI
533
+ },
534
+ // Define the fields for this global
535
+ fields: [
536
+ // Add your fields here
537
+ {fields}
538
+ ],
539
+ // Optional: Add hooks, access control, etc.
540
+ ...{rest}
541
+ };`;
542
+ }
543
+ else if (baseName === 'PayloadConfig' || baseName === 'Config' || baseName === 'SanitizedConfig') {
544
+ template = `// Main Payload CMS configuration
545
+ export default {
546
+ admin: {
547
+ user: 'users', // Collection used for authentication
548
+ meta: {
549
+ titleSuffix: '- My Payload App', // Suffix for browser tab titles
550
+ },
551
+ },
552
+ collections: [
553
+ // Reference your collections here
554
+ ],
555
+ globals: [
556
+ // Reference your globals here
557
+ ],
558
+ // Optional: Add plugins, localization, etc.
559
+ ...{rest}
560
+ };`;
561
+ }
562
+ else if (baseName === 'Field' || baseName.endsWith('Field')) {
563
+ const typeProp = 'getProperty' in decl ? decl.getProperty('type') : undefined;
564
+ const typeEnum = typeProp && typeProp.getType().isUnion()
565
+ ? typeProp.getType().getUnionTypes().map((t) => t.isLiteral() ? t.getLiteralValueOrThrow() : null)
566
+ : baseName === 'Field' ? [
567
+ 'text', 'number', 'date', 'email', 'textarea', 'relationship', 'array', 'richText',
568
+ 'code', 'json', 'select', 'radio', 'point', 'blocks', 'join', 'upload', 'group'
569
+ ] : [baseName.toLowerCase()];
570
+ const fieldType = baseName.replace('Field', '').toLowerCase();
571
+ if (fieldType === 'text' || fieldType === '') {
572
+ template = `{
573
+ name: '{name}', // Database field name
574
+ type: 'text',
575
+ label: 'Text Field', // Label shown in the admin UI
576
+ required: true,
577
+ minLength: 2,
578
+ maxLength: 100,
579
+ // Optional: Add custom validation
580
+ validate: (value, { siblingData }) => {
581
+ if (value && value.toLowerCase().includes('forbidden')) {
582
+ return 'This field cannot contain the word "forbidden"';
583
+ }
584
+ return true; // Return true if valid
585
+ },
586
+ // Optional: Admin UI configuration
587
+ admin: {
588
+ description: 'Enter text content here',
589
+ position: 'sidebar',
590
+ width: '50%',
591
+ readOnly: false,
592
+ hidden: false
593
+ }
594
+ }`;
595
+ }
596
+ else if (fieldType === 'email') {
597
+ template = `{
598
+ name: '{name}', // Database field name
599
+ type: 'email',
600
+ label: 'Email Field', // Label shown in the admin UI
601
+ required: true,
602
+ // Optional: Add custom validation beyond the built-in email validation
603
+ validate: (value) => {
604
+ if (value && !value.includes('@example.com')) {
605
+ return 'Only example.com email addresses are allowed';
606
+ }
607
+ return true; // Return true if valid
608
+ },
609
+ // Optional: Admin UI configuration
610
+ admin: {
611
+ description: 'Enter a valid email address',
612
+ position: 'sidebar'
613
+ }
614
+ }`;
615
+ }
616
+ else if (fieldType === 'number') {
617
+ template = `{
618
+ name: '{name}', // Database field name
619
+ type: 'number',
620
+ label: 'Number Field', // Label shown in the admin UI
621
+ required: true,
622
+ min: 0,
623
+ max: 100,
624
+ // Optional: Add custom validation
625
+ validate: (value) => {
626
+ if (value && value % 1 !== 0) {
627
+ return 'Please enter a whole number';
628
+ }
629
+ return true; // Return true if valid
630
+ },
631
+ // Optional: Admin UI configuration
632
+ admin: {
633
+ description: 'Enter a numeric value',
634
+ width: '25%'
635
+ }
636
+ }`;
637
+ }
638
+ else if (fieldType === 'date') {
639
+ template = `{
640
+ name: '{name}', // Database field name
641
+ type: 'date',
642
+ label: 'Date Field', // Label shown in the admin UI
643
+ required: true,
644
+ admin: {
645
+ date: {
646
+ pickerAppearance: 'dayAndTime', // 'dayOnly', 'timeOnly', or 'dayAndTime'
647
+ },
648
+ },
649
+ // Optional: Add custom validation
650
+ validate: (value) => {
651
+ const date = new Date(value);
652
+ if (date < new Date()) {
653
+ return 'Date must be in the future';
654
+ }
655
+ return true; // Return true if valid
656
+ },
657
+ }`;
658
+ }
659
+ else if (fieldType === 'textarea') {
660
+ template = `{
661
+ name: '{name}', // Database field name
662
+ type: 'textarea',
663
+ label: 'Textarea Field', // Label shown in the admin UI
664
+ required: true,
665
+ minLength: 2,
666
+ maxLength: 500,
667
+ // Optional: Admin UI configuration
668
+ admin: {
669
+ description: 'Enter longer text content here'
670
+ }
671
+ }`;
672
+ }
673
+ else if (fieldType === 'relationship' || fieldType === 'polymorphicrelationship' || fieldType === 'singlerelationship') {
674
+ template = `{
675
+ name: '{name}', // Database field name
676
+ type: 'relationship',
677
+ label: 'Relationship Field', // Label shown in the admin UI
678
+ required: true,
679
+ relationTo: 'collection-name', // Replace with your collection slug
680
+ hasMany: false, // Set to true for multiple relationships
681
+ // Optional: Admin UI configuration
682
+ admin: {
683
+ description: 'Select a related document'
684
+ }
685
+ }`;
686
+ }
687
+ else if (fieldType === 'richtext') {
688
+ template = `{
689
+ name: '{name}', // Database field name
690
+ type: 'richText',
691
+ label: 'Rich Text Field', // Label shown in the admin UI
692
+ required: true,
693
+ admin: {
694
+ elements: ['h2', 'h3', 'link', 'ol', 'ul', 'indent'],
695
+ leaves: ['bold', 'italic', 'underline'],
696
+ },
697
+ // Optional: Add custom validation
698
+ validate: (value) => {
699
+ if (value && JSON.stringify(value).length < 20) {
700
+ return 'Please enter more content';
701
+ }
702
+ return true; // Return true if valid
703
+ },
704
+ }`;
705
+ }
706
+ else if (fieldType === 'select') {
707
+ template = `{
708
+ name: '{name}', // Database field name
709
+ type: 'select',
710
+ label: 'Select Field', // Label shown in the admin UI
711
+ required: true,
712
+ options: [
713
+ { label: 'Option 1', value: 'option1' },
714
+ { label: 'Option 2', value: 'option2' },
715
+ { label: 'Option 3', value: 'option3' }
716
+ ],
717
+ // Optional: Admin UI configuration
718
+ admin: {
719
+ description: 'Select one option from the list'
720
+ }
721
+ }`;
722
+ }
723
+ else if (fieldType === 'radio') {
724
+ template = `{
725
+ name: '{name}', // Database field name
726
+ type: 'radio',
727
+ label: 'Radio Field', // Label shown in the admin UI
728
+ required: true,
729
+ options: [
730
+ {
731
+ label: 'Option One',
732
+ value: 'option-one',
733
+ },
734
+ {
735
+ label: 'Option Two',
736
+ value: 'option-two',
737
+ },
738
+ ],
739
+ defaultValue: 'option-one',
740
+ // Optional: Add custom validation
741
+ validate: (value) => {
742
+ // Custom validation logic
743
+ return true; // Return true if valid
744
+ },
745
+ }`;
746
+ }
747
+ else if (fieldType === 'checkbox') {
748
+ template = `{
749
+ name: '{name}', // Database field name
750
+ type: 'checkbox',
751
+ label: 'Checkbox Field', // Label shown in the admin UI
752
+ defaultValue: false,
753
+ // Optional: Add custom validation
754
+ validate: (value, { siblingData }) => {
755
+ if (siblingData.requiresAgreement && value !== true) {
756
+ return 'You must agree to the terms';
757
+ }
758
+ return true; // Return true if valid
759
+ },
760
+ }`;
761
+ }
762
+ else if (fieldType === 'group') {
763
+ template = `{
764
+ name: '{name}', // Database field name
765
+ type: 'group',
766
+ label: 'Group Field', // Label shown in the admin UI
767
+ fields: [
768
+ {
769
+ name: 'groupedField1',
770
+ type: 'text',
771
+ required: true,
772
+ },
773
+ {
774
+ name: 'groupedField2',
775
+ type: 'number',
776
+ },
777
+ // Add more fields as needed
778
+ ],
779
+ // Optional: Add custom validation for the entire group
780
+ validate: (value) => {
781
+ if (value && (!value.groupedField1 || !value.groupedField2)) {
782
+ return 'Both fields in the group are required';
783
+ }
784
+ return true; // Return true if valid
785
+ },
786
+ }`;
787
+ }
788
+ else if (fieldType === 'blocks') {
789
+ template = `{
790
+ name: '{name}', // Database field name
791
+ type: 'blocks',
792
+ label: 'Blocks Field', // Label shown in the admin UI
793
+ minRows: 1,
794
+ maxRows: 10,
795
+ blocks: [
796
+ {
797
+ slug: 'textBlock',
798
+ fields: [
799
+ {
800
+ name: 'text',
801
+ type: 'richText',
802
+ required: true,
803
+ },
804
+ ],
805
+ },
806
+ {
807
+ slug: 'imageBlock',
808
+ fields: [
809
+ {
810
+ name: 'image',
811
+ type: 'upload',
812
+ relationTo: 'media',
813
+ required: true,
814
+ },
815
+ ],
816
+ },
817
+ // Add more block types as needed
818
+ ],
819
+ // Optional: Add custom validation
820
+ validate: (value) => {
821
+ if (value && value.length > 0) {
822
+ // Validate the blocks as a whole
823
+ return true;
824
+ }
825
+ return 'Please add at least one block';
826
+ },
827
+ }`;
828
+ }
829
+ else if (fieldType === 'upload') {
830
+ template = `{
831
+ name: '{name}', // Database field name
832
+ type: 'upload',
833
+ label: 'Upload Field', // Label shown in the admin UI
834
+ relationTo: 'media', // The collection to upload to
835
+ required: true,
836
+ // Optional: Add custom validation
837
+ validate: (value) => {
838
+ if (!value) {
839
+ return 'Please upload a file';
840
+ }
841
+ return true; // Return true if valid
842
+ },
843
+ }`;
844
+ }
845
+ else if (fieldType === 'code') {
846
+ template = `{
847
+ name: '{name}', // Database field name
848
+ type: 'code',
849
+ label: 'Code Field', // Label shown in the admin UI
850
+ admin: {
851
+ language: 'javascript', // The language for syntax highlighting
852
+ },
853
+ // Optional: Add custom validation
854
+ validate: (value) => {
855
+ if (value && value.length < 10) {
856
+ return 'Please enter more code';
857
+ }
858
+ return true; // Return true if valid
859
+ },
860
+ }`;
861
+ }
862
+ else if (fieldType === 'json') {
863
+ template = `{
864
+ name: '{name}', // Database field name
865
+ type: 'json',
866
+ label: 'JSON Field', // Label shown in the admin UI
867
+ // Optional: Add custom validation
868
+ validate: (value) => {
869
+ try {
870
+ if (typeof value === 'string') {
871
+ JSON.parse(value);
872
+ }
873
+ return true; // Return true if valid
874
+ } catch (err) {
875
+ return 'Invalid JSON format';
876
+ }
877
+ },
878
+ }`;
879
+ }
880
+ else {
881
+ template = `{
882
+ name: '{name}', // Database field name
883
+ type: '${fieldType || '{type}'}',
884
+ label: '${fieldType ? fieldType.charAt(0).toUpperCase() + fieldType.slice(1) : 'Custom'} Field', // Label shown in the admin UI
885
+ required: true,
886
+ // Add field-specific properties here
887
+
888
+ // Optional: Add custom validation
889
+ validate: (value, { siblingData, operation }) => {
890
+ // Custom validation logic based on the value, sibling data, or operation
891
+ if (!value && operation === 'create') {
892
+ return 'This field is required for new records';
893
+ }
894
+ return true; // Return true if valid
895
+ },
896
+ ...{rest}
897
+ }`;
898
+ }
899
+ if (schema.type === 'any' || !schema.properties) {
900
+ schema.type = 'object';
901
+ schema.properties = {
902
+ name: { type: 'string', description: 'The name of the field, used as the property name in the database' },
903
+ type: {
904
+ type: 'string',
905
+ enum: baseName === 'Field' ?
906
+ ['text', 'number', 'email', 'textarea', 'date', 'checkbox', 'select', 'radio', 'relationship', 'array', 'richText', 'code', 'json', 'point', 'blocks', 'group', 'upload'] :
907
+ [baseName.replace('Field', '').toLowerCase()],
908
+ description: 'The type of field'
909
+ },
910
+ label: { type: 'string', description: 'The label shown in the admin UI' },
911
+ required: { type: 'boolean', description: 'Whether this field is required' },
912
+ admin: {
913
+ type: 'object',
914
+ description: 'Admin panel configuration for this field',
915
+ properties: {
916
+ description: { type: 'string', description: 'Additional description shown in the admin UI' },
917
+ position: { type: 'string', enum: ['sidebar'], description: 'Position of the field in the admin UI' },
918
+ width: { type: 'string', description: 'Width of the field in the admin UI' },
919
+ style: { type: 'object', description: 'Custom CSS styles for the field' },
920
+ className: { type: 'string', description: 'CSS class name for the field' },
921
+ readOnly: { type: 'boolean', description: 'Whether the field is read-only in the admin UI' },
922
+ hidden: { type: 'boolean', description: 'Whether the field is hidden in the admin UI' },
923
+ condition: { type: 'string', description: 'Condition for showing/hiding the field' }
924
+ }
925
+ }
926
+ };
927
+ if (baseName === 'TextField' || baseName === 'EmailField' || baseName === 'TextareaField') {
928
+ schema.properties.minLength = { type: 'number', description: 'Minimum length of the text' };
929
+ schema.properties.maxLength = { type: 'number', description: 'Maximum length of the text' };
930
+ schema.properties.validate = {
931
+ type: 'string',
932
+ description: 'Custom validation function that returns true if valid or an error message string if invalid'
933
+ };
934
+ }
935
+ else if (baseName === 'NumberField') {
936
+ schema.properties.min = { type: 'number', description: 'Minimum value' };
937
+ schema.properties.max = { type: 'number', description: 'Maximum value' };
938
+ schema.properties.validate = {
939
+ type: 'string',
940
+ description: 'Custom validation function that returns true if valid or an error message string if invalid'
941
+ };
942
+ }
943
+ else if (baseName === 'DateField') {
944
+ schema.properties.defaultValue = { type: 'string', description: 'Default date value' };
945
+ schema.properties.validate = {
946
+ type: 'string',
947
+ description: 'Custom validation function that returns true if valid or an error message string if invalid'
948
+ };
949
+ }
950
+ else if (baseName === 'RelationshipField' || baseName === 'PolymorphicRelationshipField') {
951
+ schema.properties.relationTo = {
952
+ oneOf: [
953
+ { type: 'string', description: 'Collection to relate to' },
954
+ { type: 'array', items: { type: 'string' }, description: 'Collections to relate to' }
955
+ ]
956
+ };
957
+ schema.properties.hasMany = { type: 'boolean', description: 'Whether this field can relate to multiple documents' };
958
+ schema.properties.validate = {
959
+ type: 'string',
960
+ description: 'Custom validation function that returns true if valid or an error message string if invalid'
961
+ };
962
+ }
963
+ else if (baseName === 'ArrayField') {
964
+ schema.properties.minRows = { type: 'number', description: 'Minimum number of rows' };
965
+ schema.properties.maxRows = { type: 'number', description: 'Maximum number of rows' };
966
+ schema.properties.validate = {
967
+ type: 'string',
968
+ description: 'Custom validation function that returns true if valid or an error message string if invalid'
969
+ };
970
+ }
971
+ else if (baseName === 'SelectField' || baseName === 'RadioField') {
972
+ schema.properties.options = {
973
+ type: 'array',
974
+ items: {
975
+ type: 'object',
976
+ properties: {
977
+ label: { type: 'string' },
978
+ value: { type: 'string' }
979
+ }
980
+ },
981
+ description: 'Options for the select/radio field'
982
+ };
983
+ schema.properties.validate = {
984
+ type: 'string',
985
+ description: 'Custom validation function that returns true if valid or an error message string if invalid'
986
+ };
987
+ }
988
+ else {
989
+ schema.properties.validate = {
990
+ type: 'string',
991
+ description: 'Custom validation function that returns true if valid or an error message string if invalid'
992
+ };
993
+ }
994
+ schema.required = ['name', 'type'];
995
+ }
996
+ else if (typeProp) {
997
+ schema.properties.type = {
998
+ type: 'string',
999
+ enum: typeEnum.filter(Boolean),
1000
+ description: 'The type of field'
1001
+ };
1002
+ if (!schema.properties.admin) {
1003
+ schema.properties.admin = {
1004
+ type: 'object',
1005
+ description: 'Admin panel configuration for this field',
1006
+ properties: {
1007
+ description: { type: 'string', description: 'Additional description shown in the admin UI' },
1008
+ position: { type: 'string', enum: ['sidebar'], description: 'Position of the field in the admin UI' },
1009
+ width: { type: 'string', description: 'Width of the field in the admin UI' },
1010
+ style: { type: 'object', description: 'Custom CSS styles for the field' },
1011
+ className: { type: 'string', description: 'CSS class name for the field' },
1012
+ readOnly: { type: 'boolean', description: 'Whether the field is read-only in the admin UI' },
1013
+ hidden: { type: 'boolean', description: 'Whether the field is hidden in the admin UI' },
1014
+ condition: { type: 'string', description: 'Condition for showing/hiding the field' }
1015
+ }
1016
+ };
1017
+ }
1018
+ schema.required = schema.required?.includes('type') ? ['name', 'type'] : ['name'];
1019
+ }
1020
+ }
1021
+ let description = `Creates a Payload CMS 3.0 ${baseName} configuration`;
1022
+ if (jsDocDescription) {
1023
+ description = `${description}\n\n${jsDocDescription}`;
1024
+ }
1025
+ let examples = '';
1026
+ if (baseName === 'CollectionConfig') {
1027
+ examples = `
1028
+ Example:
1029
+ \`\`\`typescript
1030
+ // Collection for a basic blog post
1031
+ export const Posts = {
1032
+ slug: 'posts',
1033
+ admin: {
1034
+ useAsTitle: 'title',
1035
+ },
1036
+ fields: [
1037
+ {
1038
+ name: 'title',
1039
+ type: 'text',
1040
+ required: true,
1041
+ },
1042
+ {
1043
+ name: 'content',
1044
+ type: 'richText',
1045
+ },
1046
+ {
1047
+ name: 'author',
1048
+ type: 'relationship',
1049
+ relationTo: 'users',
1050
+ },
1051
+ ],
1052
+ }
1053
+ \`\`\``;
1054
+ }
1055
+ else if (baseName.endsWith('Field')) {
1056
+ const fieldType = baseName.replace('Field', '').toLowerCase();
1057
+ if (fieldType === 'text') {
1058
+ examples = `
1059
+ Example:
1060
+ \`\`\`typescript
1061
+ {
1062
+ name: 'title',
1063
+ type: 'text',
1064
+ required: true,
1065
+ label: 'Post Title',
1066
+ minLength: 10,
1067
+ maxLength: 100,
1068
+ // Custom validation example
1069
+ validate: (value, { siblingData }) => {
1070
+ if (value && value.toLowerCase().includes('forbidden')) {
1071
+ return 'Title cannot contain the word "forbidden"';
1072
+ }
1073
+ return true;
1074
+ }
1075
+ }
1076
+ \`\`\``;
1077
+ }
1078
+ else if (fieldType === 'relationship') {
1079
+ examples = `
1080
+ Example:
1081
+ \`\`\`typescript
1082
+ {
1083
+ name: 'author',
1084
+ type: 'relationship',
1085
+ relationTo: 'users',
1086
+ hasMany: false,
1087
+ required: true,
1088
+ // Custom validation example
1089
+ validate: async (value, { req }) => {
1090
+ // Check if the related document exists and is published
1091
+ if (value) {
1092
+ const relatedDoc = await req.payload.findByID({
1093
+ collection: 'users',
1094
+ id: value,
1095
+ });
1096
+
1097
+ if (!relatedDoc) {
1098
+ return 'Selected user does not exist';
1099
+ }
1100
+ }
1101
+ return true;
1102
+ }
1103
+ }
1104
+ \`\`\``;
1105
+ }
1106
+ else if (fieldType === 'number') {
1107
+ examples = `
1108
+ Example:
1109
+ \`\`\`typescript
1110
+ {
1111
+ name: 'price',
1112
+ type: 'number',
1113
+ required: true,
1114
+ label: 'Product Price',
1115
+ min: 0,
1116
+ max: 1000,
1117
+ // Custom validation example
1118
+ validate: (value) => {
1119
+ if (value && value % 1 !== 0) {
1120
+ return 'Price must be a whole number';
1121
+ }
1122
+ return true;
1123
+ }
1124
+ }
1125
+ \`\`\``;
1126
+ }
1127
+ else if (fieldType === 'select') {
1128
+ examples = `
1129
+ Example:
1130
+ \`\`\`typescript
1131
+ {
1132
+ name: 'status',
1133
+ type: 'select',
1134
+ required: true,
1135
+ label: 'Status',
1136
+ options: [
1137
+ { label: 'Draft', value: 'draft' },
1138
+ { label: 'Published', value: 'published' },
1139
+ { label: 'Archived', value: 'archived' }
1140
+ ],
1141
+ defaultValue: 'draft',
1142
+ // Custom validation example
1143
+ validate: (value, { siblingData }) => {
1144
+ if (value === 'published' && !siblingData.publishedAt) {
1145
+ return 'Cannot set status to published without a publish date';
1146
+ }
1147
+ return true;
1148
+ }
1149
+ }
1150
+ \`\`\``;
1151
+ }
1152
+ else if (fieldType === 'array') {
1153
+ examples = `
1154
+ Example:
1155
+ \`\`\`typescript
1156
+ {
1157
+ name: 'items',
1158
+ type: 'array',
1159
+ label: 'Items',
1160
+ minRows: 1,
1161
+ maxRows: 10,
1162
+ fields: [
1163
+ {
1164
+ name: 'name',
1165
+ type: 'text',
1166
+ required: true
1167
+ },
1168
+ {
1169
+ name: 'quantity',
1170
+ type: 'number',
1171
+ required: true,
1172
+ min: 1
1173
+ }
1174
+ ],
1175
+ // Custom validation example
1176
+ validate: (value) => {
1177
+ if (value && value.length > 0) {
1178
+ const totalQuantity = value.reduce((sum, item) => sum + (item.quantity || 0), 0);
1179
+ if (totalQuantity > 100) {
1180
+ return 'Total quantity cannot exceed 100';
1181
+ }
1182
+ }
1183
+ return true;
1184
+ }
1185
+ }
1186
+ \`\`\``;
1187
+ }
1188
+ else {
1189
+ examples = `
1190
+ Example:
1191
+ \`\`\`typescript
1192
+ {
1193
+ name: '${fieldType}Field',
1194
+ type: '${fieldType}',
1195
+ required: true,
1196
+ label: '${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)} Field',
1197
+ // Custom validation example
1198
+ validate: (value, { siblingData, operation }) => {
1199
+ // Validation logic based on the value, sibling data, or operation
1200
+ if (operation === 'create' && !value) {
1201
+ return 'This field is required for new records';
1202
+ }
1203
+ return true;
1204
+ }
1205
+ }
1206
+ \`\`\``;
1207
+ }
1208
+ }
1209
+ else if (baseName.includes('Hook')) {
1210
+ if (baseName.includes('BeforeChange')) {
1211
+ template = `/**
1212
+ * This hook runs before a document is saved to the database
1213
+ * It allows you to modify the data or perform validation
1214
+ */
1215
+ const beforeChangeHook = ({ data, req, operation, originalDoc }) => {
1216
+ // 'data' contains the data being saved
1217
+ // 'req' is the Express request object with the Payload instance
1218
+ // 'operation' is either 'create' or 'update'
1219
+ // 'originalDoc' is the document before changes (for updates)
1220
+
1221
+ // Example: Add a timestamp
1222
+ return {
1223
+ ...data,
1224
+ modifiedAt: new Date().toISOString(),
1225
+ modifiedBy: req.user?.id,
1226
+ };
1227
+ };`;
1228
+ }
1229
+ else if (baseName.includes('AfterChange')) {
1230
+ template = `/**
1231
+ * This hook runs after a document has been saved to the database
1232
+ * It allows you to perform side effects but cannot modify the saved data
1233
+ */
1234
+ const afterChangeHook = ({ doc, req, operation, previousDoc }) => {
1235
+ // 'doc' contains the saved document
1236
+ // 'req' is the Express request object with the Payload instance
1237
+ // 'operation' is either 'create' or 'update'
1238
+ // 'previousDoc' is the document before changes (for updates)
1239
+
1240
+ // Example: Send a notification
1241
+ if (operation === 'create') {
1242
+ // Send notification about new document
1243
+ logger.info(\`New document created: \${doc.id}\`);
1244
+ }
1245
+
1246
+ // Return the document (cannot be modified)
1247
+ return doc;
1248
+ };`;
1249
+ }
1250
+ else if (baseName.includes('BeforeDelete')) {
1251
+ template = `/**
1252
+ * This hook runs before a document is deleted
1253
+ * It allows you to perform validation or prevent deletion
1254
+ */
1255
+ const beforeDeleteHook = ({ req, id }) => {
1256
+ // 'req' is the Express request object with the Payload instance
1257
+ // 'id' is the ID of the document being deleted
1258
+
1259
+ // Example: Check if deletion is allowed
1260
+ // If you return a string, it will prevent deletion with that error message
1261
+ // If you throw an error, it will prevent deletion with that error
1262
+
1263
+ // To allow deletion, return undefined or void
1264
+ return;
1265
+ };`;
1266
+ }
1267
+ else if (baseName.includes('AfterDelete')) {
1268
+ template = `/**
1269
+ * This hook runs after a document has been deleted
1270
+ * It allows you to perform side effects
1271
+ */
1272
+ const afterDeleteHook = ({ req, id, doc }) => {
1273
+ // 'req' is the Express request object with the Payload instance
1274
+ // 'id' is the ID of the document that was deleted
1275
+ // 'doc' is the document that was deleted
1276
+
1277
+ // Example: Clean up related data
1278
+ logger.info(\`Document deleted: \${id}\`);
1279
+
1280
+ // No return value is expected
1281
+ };`;
1282
+ }
1283
+ else if (baseName.includes('BeforeValidate')) {
1284
+ template = `/**
1285
+ * This hook runs before validation occurs
1286
+ * It allows you to modify the data before validation
1287
+ */
1288
+ const beforeValidateHook = ({ data, req, operation, originalDoc }) => {
1289
+ // 'data' contains the data to be validated
1290
+ // 'req' is the Express request object with the Payload instance
1291
+ // 'operation' is either 'create' or 'update'
1292
+ // 'originalDoc' is the document before changes (for updates)
1293
+
1294
+ // Example: Set default values
1295
+ return {
1296
+ ...data,
1297
+ status: data.status || 'draft',
1298
+ };
1299
+ };`;
1300
+ }
1301
+ else if (baseName.includes('BeforeRead') || baseName.includes('AfterRead')) {
1302
+ template = `/**
1303
+ * This hook runs before/after documents are returned from the database
1304
+ * It allows you to modify the data before it's sent to the client
1305
+ */
1306
+ const readHook = ({ doc, req }) => {
1307
+ // 'doc' contains the document(s) being read
1308
+ // 'req' is the Express request object with the Payload instance
1309
+
1310
+ // Example: Add computed properties
1311
+ return {
1312
+ ...doc,
1313
+ computedProperty: \`\${doc.firstName} \${doc.lastName}\`,
1314
+ };
1315
+ };`;
1316
+ }
1317
+ else {
1318
+ template = `/**
1319
+ * Generic Payload CMS hook
1320
+ * See documentation for specific hook parameters
1321
+ */
1322
+ const hook = (args) => {
1323
+ // Extract relevant properties from args based on hook type
1324
+ const { req } = args;
1325
+
1326
+ // Example: Log hook execution
1327
+ logger.info('Hook executed');
1328
+
1329
+ // For hooks that modify data, return the modified data
1330
+ // For other hooks, return as appropriate for the hook type
1331
+ return args.data ? { ...args.data, modified: true } : undefined;
1332
+ };`;
1333
+ }
1334
+ if (schema.type === 'any' || !schema.properties) {
1335
+ schema.type = 'object';
1336
+ schema.properties = {
1337
+ description: { type: 'string', description: 'Description of what this hook does' }
1338
+ };
1339
+ }
1340
+ }
1341
+ else if (baseName === 'Access') {
1342
+ template = `/**
1343
+ * Access control function to determine if the operation is allowed
1344
+ * Returns true if access is granted, false if denied
1345
+ * Can also return a string with an error message if denied
1346
+ */
1347
+ const accessControl = ({ req, id, data, doc }) => {
1348
+ // 'req' is the Express request object with the Payload instance and user
1349
+ // 'id' is the ID of the document being accessed (for operations on existing documents)
1350
+ // 'data' contains the data for create/update operations
1351
+ // 'doc' is the existing document for update operations
1352
+
1353
+ // Example: Only allow access if user is logged in
1354
+ if (!req.user) {
1355
+ return false; // Or return 'You must be logged in'
1356
+ }
1357
+
1358
+ // Example: Check user roles
1359
+ if (req.user.role === 'admin') {
1360
+ return true; // Admins have full access
1361
+ }
1362
+
1363
+ // Example: Users can only access their own documents
1364
+ if (id && doc && doc.createdBy === req.user.id) {
1365
+ return true;
1366
+ }
1367
+
1368
+ // Deny access with a custom message
1369
+ return 'You do not have permission to access this resource';
1370
+ };`;
1371
+ if (schema.type === 'any' || !schema.properties) {
1372
+ schema.type = 'object';
1373
+ schema.properties = {
1374
+ description: { type: 'string', description: 'Description of this access control function' }
1375
+ };
1376
+ }
1377
+ }
1378
+ else if (baseName === 'CollectionAdminOptions') {
1379
+ template = `/**
1380
+ * Admin UI options for a collection
1381
+ */
1382
+ {
1383
+ // Field to use as the title in the admin UI
1384
+ useAsTitle: 'title',
1385
+
1386
+ // Default columns to show in the admin UI list view
1387
+ defaultColumns: ['title', 'status', 'createdAt'],
1388
+
1389
+ // Group collections in the admin UI sidebar
1390
+ group: 'Content',
1391
+
1392
+ // Custom admin components (requires importing from your components)
1393
+ // components: {
1394
+ // views: {
1395
+ // List: MyCustomListView,
1396
+ // },
1397
+ // },
1398
+
1399
+ // Additional options
1400
+ ...{rest}
1401
+ }`;
1402
+ }
1403
+ if (examples) {
1404
+ description = `${description}\n${examples}`;
1405
+ }
1406
+ toolsArray.push({
1407
+ name: toolName,
1408
+ description: description,
1409
+ inputSchema: schema,
1410
+ template
1411
+ });
1412
+ }
1413
+ generatePayloadTools().catch(error => {
1414
+ console.error('Failed to generate tools:', error.message);
1415
+ process.exit(1);
1416
+ });
1417
+ //# sourceMappingURL=generate-tools.js.map