@payloadcms/plugin-mcp 0.0.1-alpha.0

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 (173) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +7 -0
  3. package/dist/collections/createApiKeysCollection.d.ts +7 -0
  4. package/dist/collections/createApiKeysCollection.d.ts.map +1 -0
  5. package/dist/collections/createApiKeysCollection.js +315 -0
  6. package/dist/collections/createApiKeysCollection.js.map +1 -0
  7. package/dist/endpoints/mcp.d.ts +4 -0
  8. package/dist/endpoints/mcp.d.ts.map +1 -0
  9. package/dist/endpoints/mcp.js +44 -0
  10. package/dist/endpoints/mcp.js.map +1 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +67 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mcp/createRequest.d.ts +3 -0
  16. package/dist/mcp/createRequest.d.ts.map +1 -0
  17. package/dist/mcp/createRequest.js +14 -0
  18. package/dist/mcp/createRequest.js.map +1 -0
  19. package/dist/mcp/getMcpHandler.d.ts +4 -0
  20. package/dist/mcp/getMcpHandler.d.ts.map +1 -0
  21. package/dist/mcp/getMcpHandler.js +179 -0
  22. package/dist/mcp/getMcpHandler.js.map +1 -0
  23. package/dist/mcp/helpers/config.d.ts +30 -0
  24. package/dist/mcp/helpers/config.d.ts.map +1 -0
  25. package/dist/mcp/helpers/config.js +217 -0
  26. package/dist/mcp/helpers/config.js.map +1 -0
  27. package/dist/mcp/helpers/conversion.d.ts +2 -0
  28. package/dist/mcp/helpers/conversion.d.ts.map +1 -0
  29. package/dist/mcp/helpers/conversion.js +5 -0
  30. package/dist/mcp/helpers/conversion.js.map +1 -0
  31. package/dist/mcp/helpers/fields.d.ts +38 -0
  32. package/dist/mcp/helpers/fields.d.ts.map +1 -0
  33. package/dist/mcp/helpers/fields.js +96 -0
  34. package/dist/mcp/helpers/fields.js.map +1 -0
  35. package/dist/mcp/helpers/fileValidation.d.ts +69 -0
  36. package/dist/mcp/helpers/fileValidation.d.ts.map +1 -0
  37. package/dist/mcp/helpers/fileValidation.js +305 -0
  38. package/dist/mcp/helpers/fileValidation.js.map +1 -0
  39. package/dist/mcp/helpers/validation.d.ts +9 -0
  40. package/dist/mcp/helpers/validation.d.ts.map +1 -0
  41. package/dist/mcp/helpers/validation.js +22 -0
  42. package/dist/mcp/helpers/validation.js.map +1 -0
  43. package/dist/mcp/registerTool.d.ts +6 -0
  44. package/dist/mcp/registerTool.d.ts.map +1 -0
  45. package/dist/mcp/registerTool.js +18 -0
  46. package/dist/mcp/registerTool.js.map +1 -0
  47. package/dist/mcp/tools/auth/auth.d.ts +4 -0
  48. package/dist/mcp/tools/auth/auth.d.ts.map +1 -0
  49. package/dist/mcp/tools/auth/auth.js +54 -0
  50. package/dist/mcp/tools/auth/auth.js.map +1 -0
  51. package/dist/mcp/tools/auth/forgotPassword.d.ts +4 -0
  52. package/dist/mcp/tools/auth/forgotPassword.d.ts.map +1 -0
  53. package/dist/mcp/tools/auth/forgotPassword.js +45 -0
  54. package/dist/mcp/tools/auth/forgotPassword.js.map +1 -0
  55. package/dist/mcp/tools/auth/login.d.ts +4 -0
  56. package/dist/mcp/tools/auth/login.d.ts.map +1 -0
  57. package/dist/mcp/tools/auth/login.js +48 -0
  58. package/dist/mcp/tools/auth/login.js.map +1 -0
  59. package/dist/mcp/tools/auth/resetPassword.d.ts +4 -0
  60. package/dist/mcp/tools/auth/resetPassword.d.ts.map +1 -0
  61. package/dist/mcp/tools/auth/resetPassword.js +46 -0
  62. package/dist/mcp/tools/auth/resetPassword.js.map +1 -0
  63. package/dist/mcp/tools/auth/unlock.d.ts +4 -0
  64. package/dist/mcp/tools/auth/unlock.d.ts.map +1 -0
  65. package/dist/mcp/tools/auth/unlock.js +45 -0
  66. package/dist/mcp/tools/auth/unlock.js.map +1 -0
  67. package/dist/mcp/tools/auth/verify.d.ts +4 -0
  68. package/dist/mcp/tools/auth/verify.d.ts.map +1 -0
  69. package/dist/mcp/tools/auth/verify.js +42 -0
  70. package/dist/mcp/tools/auth/verify.js.map +1 -0
  71. package/dist/mcp/tools/collection/create.d.ts +10 -0
  72. package/dist/mcp/tools/collection/create.d.ts.map +1 -0
  73. package/dist/mcp/tools/collection/create.js +159 -0
  74. package/dist/mcp/tools/collection/create.js.map +1 -0
  75. package/dist/mcp/tools/collection/delete.d.ts +10 -0
  76. package/dist/mcp/tools/collection/delete.d.ts.map +1 -0
  77. package/dist/mcp/tools/collection/delete.js +162 -0
  78. package/dist/mcp/tools/collection/delete.js.map +1 -0
  79. package/dist/mcp/tools/collection/find.d.ts +10 -0
  80. package/dist/mcp/tools/collection/find.d.ts.map +1 -0
  81. package/dist/mcp/tools/collection/find.js +162 -0
  82. package/dist/mcp/tools/collection/find.js.map +1 -0
  83. package/dist/mcp/tools/collection/update.d.ts +10 -0
  84. package/dist/mcp/tools/collection/update.d.ts.map +1 -0
  85. package/dist/mcp/tools/collection/update.js +206 -0
  86. package/dist/mcp/tools/collection/update.js.map +1 -0
  87. package/dist/mcp/tools/config/find.d.ts +10 -0
  88. package/dist/mcp/tools/config/find.d.ts.map +1 -0
  89. package/dist/mcp/tools/config/find.js +94 -0
  90. package/dist/mcp/tools/config/find.js.map +1 -0
  91. package/dist/mcp/tools/config/update.d.ts +10 -0
  92. package/dist/mcp/tools/config/update.d.ts.map +1 -0
  93. package/dist/mcp/tools/config/update.js +212 -0
  94. package/dist/mcp/tools/config/update.js.map +1 -0
  95. package/dist/mcp/tools/job/create.d.ts +10 -0
  96. package/dist/mcp/tools/job/create.d.ts.map +1 -0
  97. package/dist/mcp/tools/job/create.js +293 -0
  98. package/dist/mcp/tools/job/create.js.map +1 -0
  99. package/dist/mcp/tools/job/run.d.ts +10 -0
  100. package/dist/mcp/tools/job/run.d.ts.map +1 -0
  101. package/dist/mcp/tools/job/run.js +147 -0
  102. package/dist/mcp/tools/job/run.js.map +1 -0
  103. package/dist/mcp/tools/job/update.d.ts +11 -0
  104. package/dist/mcp/tools/job/update.d.ts.map +1 -0
  105. package/dist/mcp/tools/job/update.js +211 -0
  106. package/dist/mcp/tools/job/update.js.map +1 -0
  107. package/dist/mcp/tools/resource/create.d.ts +6 -0
  108. package/dist/mcp/tools/resource/create.d.ts.map +1 -0
  109. package/dist/mcp/tools/resource/create.js +75 -0
  110. package/dist/mcp/tools/resource/create.js.map +1 -0
  111. package/dist/mcp/tools/resource/delete.d.ts +5 -0
  112. package/dist/mcp/tools/resource/delete.d.ts.map +1 -0
  113. package/dist/mcp/tools/resource/delete.js +140 -0
  114. package/dist/mcp/tools/resource/delete.js.map +1 -0
  115. package/dist/mcp/tools/resource/find.d.ts +5 -0
  116. package/dist/mcp/tools/resource/find.d.ts.map +1 -0
  117. package/dist/mcp/tools/resource/find.js +119 -0
  118. package/dist/mcp/tools/resource/find.js.map +1 -0
  119. package/dist/mcp/tools/resource/update.d.ts +6 -0
  120. package/dist/mcp/tools/resource/update.d.ts.map +1 -0
  121. package/dist/mcp/tools/resource/update.js +201 -0
  122. package/dist/mcp/tools/resource/update.js.map +1 -0
  123. package/dist/mcp/tools/schemas.d.ts +374 -0
  124. package/dist/mcp/tools/schemas.d.ts.map +1 -0
  125. package/dist/mcp/tools/schemas.js +201 -0
  126. package/dist/mcp/tools/schemas.js.map +1 -0
  127. package/dist/types.d.ts +379 -0
  128. package/dist/types.d.ts.map +1 -0
  129. package/dist/types.js +3 -0
  130. package/dist/types.js.map +1 -0
  131. package/dist/utils/camelCase.d.ts +9 -0
  132. package/dist/utils/camelCase.d.ts.map +1 -0
  133. package/dist/utils/camelCase.js +11 -0
  134. package/dist/utils/camelCase.js.map +1 -0
  135. package/dist/utils/convertCollectionSchemaToZod.d.ts +3 -0
  136. package/dist/utils/convertCollectionSchemaToZod.d.ts.map +1 -0
  137. package/dist/utils/convertCollectionSchemaToZod.js +30 -0
  138. package/dist/utils/convertCollectionSchemaToZod.js.map +1 -0
  139. package/package.json +64 -0
  140. package/src/collections/createApiKeysCollection.ts +393 -0
  141. package/src/endpoints/mcp.ts +60 -0
  142. package/src/index.ts +86 -0
  143. package/src/mcp/createRequest.ts +13 -0
  144. package/src/mcp/getMcpHandler.ts +433 -0
  145. package/src/mcp/helpers/config.ts +326 -0
  146. package/src/mcp/helpers/conversion.ts +3 -0
  147. package/src/mcp/helpers/fields.ts +158 -0
  148. package/src/mcp/helpers/fileValidation.ts +417 -0
  149. package/src/mcp/helpers/validation.ts +32 -0
  150. package/src/mcp/registerTool.ts +22 -0
  151. package/src/mcp/tools/auth/auth.ts +69 -0
  152. package/src/mcp/tools/auth/forgotPassword.ts +68 -0
  153. package/src/mcp/tools/auth/login.ts +70 -0
  154. package/src/mcp/tools/auth/resetPassword.ts +59 -0
  155. package/src/mcp/tools/auth/unlock.ts +62 -0
  156. package/src/mcp/tools/auth/verify.ts +55 -0
  157. package/src/mcp/tools/collection/create.ts +236 -0
  158. package/src/mcp/tools/collection/delete.ts +227 -0
  159. package/src/mcp/tools/collection/find.ts +222 -0
  160. package/src/mcp/tools/collection/update.ts +288 -0
  161. package/src/mcp/tools/config/find.ts +126 -0
  162. package/src/mcp/tools/config/update.ts +282 -0
  163. package/src/mcp/tools/job/create.ts +420 -0
  164. package/src/mcp/tools/job/run.ts +189 -0
  165. package/src/mcp/tools/job/update.ts +319 -0
  166. package/src/mcp/tools/resource/create.ts +121 -0
  167. package/src/mcp/tools/resource/delete.ts +210 -0
  168. package/src/mcp/tools/resource/find.ts +194 -0
  169. package/src/mcp/tools/resource/update.ts +314 -0
  170. package/src/mcp/tools/schemas.ts +373 -0
  171. package/src/types.ts +405 -0
  172. package/src/utils/camelCase.ts +12 -0
  173. package/src/utils/convertCollectionSchemaToZod.ts +35 -0
@@ -0,0 +1,282 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import type { PayloadRequest } from 'payload'
3
+
4
+ import { readFileSync, writeFileSync } from 'fs'
5
+
6
+ import {
7
+ addCollectionToConfig,
8
+ removeCollectionFromConfig,
9
+ updateAdminConfig,
10
+ updateDatabaseConfig,
11
+ updatePluginsConfig,
12
+ } from '../../helpers/config.js'
13
+ import { toolSchemas } from '../schemas.js'
14
+
15
+ export const updateConfig = (
16
+ req: PayloadRequest,
17
+ verboseLogs: boolean,
18
+ configFilePath: string,
19
+ updateType: string,
20
+ collectionName?: string,
21
+ adminConfig?: any,
22
+ databaseConfig?: any,
23
+ pluginUpdates?: any,
24
+ generalConfig?: any,
25
+ newContent?: string,
26
+ ) => {
27
+ const payload = req.payload
28
+ if (verboseLogs) {
29
+ payload.logger.info(`[payload-mcp] Updating config with update type: ${updateType}`)
30
+ }
31
+
32
+ // Security check: ensure we're working with the specified config file
33
+ if (!configFilePath.startsWith(process.cwd()) && !configFilePath.startsWith('/')) {
34
+ payload.logger.error(`[payload-mcp] Invalid config path attempted: ${configFilePath}`)
35
+ return {
36
+ content: [
37
+ {
38
+ type: 'text' as const,
39
+ text: '❌ **Error**: Invalid config path',
40
+ },
41
+ ],
42
+ }
43
+ }
44
+
45
+ try {
46
+ // Read current config
47
+ let currentContent: string
48
+ try {
49
+ currentContent = readFileSync(configFilePath, 'utf8')
50
+ } catch (_ignore) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: 'text' as const,
55
+ text: `❌ **Error**: Config file not found: ${configFilePath}`,
56
+ },
57
+ ],
58
+ }
59
+ }
60
+
61
+ let updatedContent: string
62
+ let updateSummary: string[] = []
63
+
64
+ switch (updateType) {
65
+ case 'add_collection':
66
+ if (!collectionName) {
67
+ return {
68
+ content: [
69
+ {
70
+ type: 'text' as const,
71
+ text: '❌ **Error**: No collection name provided for add_collection update type',
72
+ },
73
+ ],
74
+ }
75
+ }
76
+ updatedContent = addCollectionToConfig(currentContent, collectionName)
77
+ updateSummary = [`Added collection: ${collectionName}`]
78
+ break
79
+
80
+ case 'remove_collection':
81
+ if (!collectionName) {
82
+ return {
83
+ content: [
84
+ {
85
+ type: 'text' as const,
86
+ text: '❌ **Error**: No collection name provided for remove_collection update type',
87
+ },
88
+ ],
89
+ }
90
+ }
91
+ updatedContent = removeCollectionFromConfig(currentContent, collectionName)
92
+ updateSummary = [`Removed collection: ${collectionName}`]
93
+ break
94
+
95
+ case 'replace_content':
96
+ if (!newContent) {
97
+ return {
98
+ content: [
99
+ {
100
+ type: 'text' as const,
101
+ text: '❌ **Error**: No new content provided for replace_content update type',
102
+ },
103
+ ],
104
+ }
105
+ }
106
+ updatedContent = newContent
107
+ updateSummary = ['Replaced entire config content']
108
+ break
109
+
110
+ case 'update_admin':
111
+ if (!adminConfig) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text' as const,
116
+ text: '❌ **Error**: No admin config provided for update_admin update type',
117
+ },
118
+ ],
119
+ }
120
+ }
121
+ updatedContent = updateAdminConfig(currentContent, adminConfig)
122
+ updateSummary = Object.keys(adminConfig).map((key) => `Updated admin config: ${key}`)
123
+ break
124
+
125
+ case 'update_database':
126
+ if (!databaseConfig) {
127
+ return {
128
+ content: [
129
+ {
130
+ type: 'text' as const,
131
+ text: '❌ **Error**: No database config provided for update_database update type',
132
+ },
133
+ ],
134
+ }
135
+ }
136
+ updatedContent = updateDatabaseConfig(currentContent, databaseConfig)
137
+ updateSummary = Object.keys(databaseConfig).map((key) => `Updated database config: ${key}`)
138
+ break
139
+
140
+ case 'update_plugins':
141
+ if (!pluginUpdates) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: 'text' as const,
146
+ text: '❌ **Error**: No plugin updates provided for update_plugins update type',
147
+ },
148
+ ],
149
+ }
150
+ }
151
+ updatedContent = updatePluginsConfig(currentContent, pluginUpdates)
152
+ updateSummary = []
153
+ if (pluginUpdates.add) {
154
+ updateSummary.push(`Added plugins: ${pluginUpdates.add.join(', ')}`)
155
+ }
156
+ if (pluginUpdates.remove) {
157
+ updateSummary.push(`Removed plugins: ${pluginUpdates.remove.join(', ')}`)
158
+ }
159
+ break
160
+
161
+ default:
162
+ return {
163
+ content: [
164
+ {
165
+ type: 'text' as const,
166
+ text: `❌ **Error**: Unknown update type: ${updateType}`,
167
+ },
168
+ ],
169
+ }
170
+ }
171
+
172
+ // Write the updated content back to the file
173
+ writeFileSync(configFilePath, updatedContent, 'utf8')
174
+ if (verboseLogs) {
175
+ payload.logger.info(`[payload-mcp] Successfully updated config file: ${configFilePath}`)
176
+ }
177
+
178
+ return {
179
+ content: [
180
+ {
181
+ type: 'text' as const,
182
+ text: `✅ **Config updated successfully!**
183
+
184
+ **File**: \`${configFilePath}\`
185
+ **Update Type**: ${updateType}
186
+
187
+ **Changes Made**:
188
+ ${updateSummary.map((summary) => `- ${summary}`).join('\n')}
189
+
190
+ **Updated Config Content:**
191
+ \`\`\`typescript
192
+ ${updatedContent}
193
+ \`\`\``,
194
+ },
195
+ ],
196
+ }
197
+ } catch (error) {
198
+ const errorMessage = (error as Error).message
199
+ payload.logger.error(`[payload-mcp] Error updating config: ${errorMessage}`)
200
+ return {
201
+ content: [
202
+ {
203
+ type: 'text' as const,
204
+ text: `❌ **Error updating config**: ${errorMessage}`,
205
+ },
206
+ ],
207
+ }
208
+ }
209
+ }
210
+
211
+ export const updateConfigTool = (
212
+ server: McpServer,
213
+ req: PayloadRequest,
214
+ verboseLogs: boolean,
215
+ configFilePath: string,
216
+ ) => {
217
+ const tool = ({
218
+ adminConfig,
219
+ collectionName,
220
+ databaseConfig,
221
+ generalConfig,
222
+ newContent,
223
+ pluginUpdates,
224
+ updateType,
225
+ }: {
226
+ adminConfig?: any
227
+ collectionName?: string
228
+ databaseConfig?: any
229
+ generalConfig?: any
230
+ newContent?: string
231
+ pluginUpdates?: any
232
+ updateType: string
233
+ }) => {
234
+ const payload = req.payload
235
+
236
+ if (verboseLogs) {
237
+ payload.logger.info(`[payload-mcp] Updating config: ${updateType}`)
238
+ }
239
+
240
+ try {
241
+ const result = updateConfig(
242
+ req,
243
+ verboseLogs,
244
+ configFilePath,
245
+ updateType,
246
+ collectionName,
247
+ adminConfig,
248
+ databaseConfig,
249
+ pluginUpdates,
250
+ generalConfig,
251
+ newContent,
252
+ )
253
+
254
+ if (verboseLogs) {
255
+ payload.logger.info(`[payload-mcp] Config update completed for: ${updateType}`)
256
+ }
257
+
258
+ return result
259
+ } catch (error) {
260
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
261
+ payload.logger.error(`[payload-mcp] Error updating config: ${errorMessage}`)
262
+
263
+ return {
264
+ content: [
265
+ {
266
+ type: 'text' as const,
267
+ text: `Error updating config: ${errorMessage}`,
268
+ },
269
+ ],
270
+ }
271
+ }
272
+ }
273
+
274
+ server.tool(
275
+ 'updateConfig',
276
+ toolSchemas.updateConfig.description,
277
+ toolSchemas.updateConfig.parameters.shape,
278
+ (args) => {
279
+ return tool(args)
280
+ },
281
+ )
282
+ }
@@ -0,0 +1,420 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import type { PayloadRequest } from 'payload'
3
+
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
5
+ import { join } from 'path'
6
+
7
+ import { validatePayloadFile } from '../../helpers/fileValidation.js'
8
+ import { toolSchemas } from '../schemas.js'
9
+
10
+ const createOrUpdateJobFile = (
11
+ req: PayloadRequest,
12
+ verboseLogs: boolean,
13
+ jobsDir: string,
14
+ jobName: string,
15
+ jobType: 'task' | 'workflow',
16
+ jobSlug: string,
17
+ camelCaseJobSlug: string,
18
+ ) => {
19
+ const payload = req.payload
20
+ const jobFilePath = join(jobsDir, `${jobName}.ts`)
21
+ const importName = `${camelCaseJobSlug}${jobType === 'task' ? 'Task' : 'Workflow'}`
22
+ const importPath = `./${jobType === 'task' ? 'tasks' : 'workflows'}/${camelCaseJobSlug}`
23
+
24
+ if (verboseLogs) {
25
+ payload.logger.info(`[payload-mcp] Processing job file: ${jobFilePath}`)
26
+ }
27
+
28
+ if (existsSync(jobFilePath)) {
29
+ if (verboseLogs) {
30
+ payload.logger.info(`[payload-mcp] Updating existing job file: ${jobFilePath}`)
31
+ }
32
+
33
+ // Update existing job file
34
+ let content = readFileSync(jobFilePath, 'utf8')
35
+
36
+ // Add import if not already present
37
+ const importStatement = `import { ${importName} } from '${importPath}'`
38
+ if (!content.includes(importStatement)) {
39
+ if (verboseLogs) {
40
+ payload.logger.info(`[payload-mcp] Adding import: ${importStatement}`)
41
+ }
42
+
43
+ // Find the last import statement and add after it
44
+ const importRegex = /import\s+(?:\S.*)?from\s+['"].*['"];?\s*\n/g
45
+ let lastImportMatch
46
+ let match
47
+ while ((match = importRegex.exec(content)) !== null) {
48
+ lastImportMatch = match
49
+ }
50
+
51
+ if (lastImportMatch) {
52
+ const insertIndex = lastImportMatch.index + lastImportMatch[0].length
53
+ content =
54
+ content.slice(0, insertIndex) + importStatement + '\n' + content.slice(insertIndex)
55
+ } else {
56
+ // No imports found, add at the beginning
57
+ content = importStatement + '\n\n' + content
58
+ }
59
+ }
60
+
61
+ // Add to the appropriate array
62
+ const arrayName = jobType === 'task' ? 'tasks' : 'workflows'
63
+ const arrayRegex = new RegExp(`(${arrayName}:\\s*\\[)([^\\]]*)(\\])`, 's')
64
+ const arrayMatch = content.match(arrayRegex)
65
+
66
+ if (arrayMatch && arrayMatch[2]) {
67
+ const existingItems = arrayMatch[2].trim()
68
+ const newItem = existingItems ? `${existingItems},\n ${importName}` : `\n ${importName}`
69
+ content = content.replace(arrayRegex, `$1${newItem}\n $3`)
70
+
71
+ if (verboseLogs) {
72
+ payload.logger.info(`[payload-mcp] Added ${importName} to ${arrayName} array`)
73
+ }
74
+ } else {
75
+ // Array doesn't exist, add it
76
+ const jobsConfigRegex = /(export\s+const\s.*JobsConfig\s*=\s*\{)([^}]*)(\})/s
77
+ const jobsConfigMatch = content.match(jobsConfigRegex)
78
+
79
+ if (jobsConfigMatch && jobsConfigMatch[2]) {
80
+ const existingConfig = jobsConfigMatch[2].trim()
81
+ const newConfig = existingConfig
82
+ ? `${existingConfig},\n ${arrayName}: [\n ${importName}\n ]`
83
+ : `\n ${arrayName}: [\n ${importName}\n ]`
84
+ content = content.replace(jobsConfigRegex, `$1${newConfig}\n$3`)
85
+
86
+ if (verboseLogs) {
87
+ payload.logger.info(`[payload-mcp] Created new ${arrayName} array with ${importName}`)
88
+ }
89
+ }
90
+ }
91
+
92
+ writeFileSync(jobFilePath, content)
93
+ if (verboseLogs) {
94
+ payload.logger.info(`[payload-mcp] Successfully updated job file: ${jobFilePath}`)
95
+ }
96
+ } else {
97
+ if (verboseLogs) {
98
+ payload.logger.info(`[payload-mcp] Creating new job file: ${jobFilePath}`)
99
+ }
100
+
101
+ // Create new job file
102
+ const camelCaseJobName = toCamelCase(jobName)
103
+ const jobFileContent = `import type { JobsConfig } from 'payload'
104
+ import { ${importName} } from '${importPath}'
105
+
106
+ export const ${camelCaseJobName}JobsConfig: JobsConfig = {
107
+ ${jobType === 'task' ? 'tasks' : 'workflows'}: [
108
+ ${importName}
109
+ ]
110
+ }
111
+ `
112
+ writeFileSync(jobFilePath, jobFileContent)
113
+ if (verboseLogs) {
114
+ payload.logger.info(`[payload-mcp] Successfully created new job file: ${jobFilePath}`)
115
+ }
116
+ }
117
+ }
118
+
119
+ // Reusable function for creating jobs
120
+ export const createJob = async (
121
+ req: PayloadRequest,
122
+ verboseLogs: boolean,
123
+ jobsDir: string,
124
+ jobName: string,
125
+ jobType: 'task' | 'workflow',
126
+ jobSlug: string,
127
+ description: string,
128
+ inputSchema: any,
129
+ outputSchema: any,
130
+ jobData: Record<string, any>,
131
+ ) => {
132
+ const payload = req.payload
133
+
134
+ if (verboseLogs) {
135
+ payload.logger.info(`[payload-mcp] Creating ${jobType}: ${jobName}`)
136
+ }
137
+
138
+ try {
139
+ // Ensure jobs directory exists
140
+ if (!existsSync(jobsDir)) {
141
+ if (verboseLogs) {
142
+ payload.logger.info(`[payload-mcp] Creating jobs directory: ${jobsDir}`)
143
+ }
144
+ mkdirSync(jobsDir, { recursive: true })
145
+ }
146
+
147
+ // Ensure subdirectories exist
148
+ const tasksDir = join(jobsDir, 'tasks')
149
+ const workflowsDir = join(jobsDir, 'workflows')
150
+
151
+ if (!existsSync(tasksDir)) {
152
+ mkdirSync(tasksDir, { recursive: true })
153
+ }
154
+ if (!existsSync(workflowsDir)) {
155
+ mkdirSync(workflowsDir, { recursive: true })
156
+ }
157
+
158
+ const camelCaseJobSlug = toCamelCase(jobSlug)
159
+ const targetDir = jobType === 'task' ? tasksDir : workflowsDir
160
+ const fileName = `${camelCaseJobSlug}.ts`
161
+ const filePath = join(targetDir, fileName)
162
+
163
+ if (verboseLogs) {
164
+ payload.logger.info(`[payload-mcp] Target file path: ${filePath}`)
165
+ }
166
+
167
+ // Security check: ensure we're working with the jobs directory
168
+ if (!filePath.startsWith(jobsDir)) {
169
+ payload.logger.error(`[payload-mcp] Invalid job path attempted: ${filePath}`)
170
+ return {
171
+ content: [
172
+ {
173
+ type: 'text' as const,
174
+ text: '❌ **Error**: Invalid job path',
175
+ },
176
+ ],
177
+ }
178
+ }
179
+
180
+ // Check if file already exists
181
+ if (existsSync(filePath)) {
182
+ if (verboseLogs) {
183
+ payload.logger.info(`[payload-mcp] Job file already exists: ${fileName}`)
184
+ }
185
+ return {
186
+ content: [
187
+ {
188
+ type: 'text' as const,
189
+ text: `❌ **Error**: Job file already exists: ${fileName}`,
190
+ },
191
+ ],
192
+ }
193
+ }
194
+
195
+ // Generate job content based on type
196
+ let jobContent: string
197
+ if (jobType === 'task') {
198
+ jobContent = generateTaskContent(
199
+ jobName,
200
+ jobSlug,
201
+ description,
202
+ inputSchema,
203
+ outputSchema,
204
+ jobData,
205
+ )
206
+ } else {
207
+ jobContent = generateWorkflowContent(
208
+ jobName,
209
+ jobSlug,
210
+ description,
211
+ inputSchema,
212
+ outputSchema,
213
+ jobData,
214
+ )
215
+ }
216
+
217
+ // Write the job file
218
+ writeFileSync(filePath, jobContent, 'utf8')
219
+ if (verboseLogs) {
220
+ payload.logger.info(`[payload-mcp] Successfully created job file: ${filePath}`)
221
+ }
222
+
223
+ // Update the main job file
224
+ createOrUpdateJobFile(req, verboseLogs, jobsDir, jobName, jobType, jobSlug, camelCaseJobSlug)
225
+
226
+ // Validate the generated file
227
+ const validationResult = await validatePayloadFile(fileName, jobType)
228
+ if (validationResult.error) {
229
+ return {
230
+ content: [
231
+ {
232
+ type: 'text' as const,
233
+ text: `❌ **Error**: Generated job has validation issues:\n\n${validationResult.error}`,
234
+ },
235
+ ],
236
+ }
237
+ }
238
+
239
+ return {
240
+ content: [
241
+ {
242
+ type: 'text' as const,
243
+ text: `✅ **Job created successfully!**
244
+
245
+ **File**: \`${fileName}\`
246
+ **Type**: \`${jobType}\`
247
+ **Slug**: \`${jobSlug}\`
248
+ **Description**: ${description}
249
+
250
+ **Generated Job Code:**
251
+ \`\`\`typescript
252
+ ${jobContent}
253
+ \`\`\``,
254
+ },
255
+ ],
256
+ }
257
+ } catch (error) {
258
+ const errorMessage = (error as Error).message
259
+ payload.logger.error(`[payload-mcp] Error creating job: ${errorMessage}`)
260
+
261
+ return {
262
+ content: [
263
+ {
264
+ type: 'text' as const,
265
+ text: `❌ **Error creating job**: ${errorMessage}`,
266
+ },
267
+ ],
268
+ }
269
+ }
270
+ }
271
+
272
+ // Helper function to generate task content
273
+ function generateTaskContent(
274
+ jobName: string,
275
+ jobSlug: string,
276
+ description: string,
277
+ inputSchema: any,
278
+ outputSchema: any,
279
+ jobData: Record<string, any>,
280
+ ): string {
281
+ const camelCaseJobSlug = toCamelCase(jobSlug)
282
+
283
+ return `import type { Task } from 'payload'
284
+
285
+ export const ${camelCaseJobSlug}Task: Task = {
286
+ slug: '${jobSlug}',
287
+ description: '${description}',
288
+ inputSchema: ${JSON.stringify(inputSchema, null, 2)},
289
+ outputSchema: ${JSON.stringify(outputSchema, null, 2)},
290
+ handler: async (input, context) => {
291
+ // TODO: Implement your task logic here
292
+ // Access input data: input.fieldName
293
+ // Access context: context.payload, context.req, etc.
294
+
295
+ // Example implementation:
296
+ const result = {
297
+ message: 'Task executed successfully',
298
+ input,
299
+ timestamp: new Date().toISOString(),
300
+ }
301
+
302
+ return result
303
+ },
304
+ }
305
+ `
306
+ }
307
+
308
+ // Helper function to generate workflow content
309
+ function generateWorkflowContent(
310
+ jobName: string,
311
+ jobSlug: string,
312
+ description: string,
313
+ inputSchema: any,
314
+ outputSchema: any,
315
+ jobData: Record<string, any>,
316
+ ): string {
317
+ const camelCaseJobSlug = toCamelCase(jobSlug)
318
+
319
+ return `import type { Workflow } from 'payload'
320
+
321
+ export const ${camelCaseJobSlug}Workflow: Workflow = {
322
+ slug: '${jobSlug}',
323
+ description: '${description}',
324
+ inputSchema: ${JSON.stringify(inputSchema, null, 2)},
325
+ outputSchema: ${JSON.stringify(outputSchema, null, 2)},
326
+ steps: [
327
+ // TODO: Define your workflow steps here
328
+ // Each step should be a function that returns a result
329
+ // Example:
330
+ // {
331
+ // name: 'step1',
332
+ // handler: async (input, context) => {
333
+ // // Step logic here
334
+ // return { result: 'step1 completed' }
335
+ // }
336
+ // }
337
+ ],
338
+ }
339
+ `
340
+ }
341
+
342
+ // Helper function to convert to camel case
343
+ function toCamelCase(str: string): string {
344
+ return str
345
+ .replace(/[-_\s]+(.)?/g, (_, chr) => (chr ? chr.toUpperCase() : ''))
346
+ .replace(/^(.)/, (_, chr) => chr.toLowerCase())
347
+ }
348
+
349
+ export const createJobTool = (
350
+ server: McpServer,
351
+ req: PayloadRequest,
352
+ verboseLogs: boolean,
353
+ jobsDir: string,
354
+ ) => {
355
+ const tool = async (
356
+ jobName: string,
357
+ jobType: 'task' | 'workflow',
358
+ jobSlug: string,
359
+ description: string,
360
+ inputSchema: any = {},
361
+ outputSchema: any = {},
362
+ jobData: Record<string, any> = {},
363
+ ) => {
364
+ if (verboseLogs) {
365
+ req.payload.logger.info(
366
+ `[payload-mcp] Create Job Tool called with: ${jobName}, ${jobType}, ${jobSlug}`,
367
+ )
368
+ }
369
+
370
+ try {
371
+ const result = await createJob(
372
+ req,
373
+ verboseLogs,
374
+ jobsDir,
375
+ jobName,
376
+ jobType,
377
+ jobSlug,
378
+ description,
379
+ inputSchema,
380
+ outputSchema,
381
+ jobData,
382
+ )
383
+
384
+ if (verboseLogs) {
385
+ req.payload.logger.info(`[payload-mcp] Create Job Tool completed successfully`)
386
+ }
387
+
388
+ return result
389
+ } catch (error) {
390
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
391
+ req.payload.logger.error(`[payload-mcp] Error in Create Job Tool: ${errorMessage}`)
392
+
393
+ return {
394
+ content: [
395
+ {
396
+ type: 'text' as const,
397
+ text: `❌ **Error in Create Job Tool**: ${errorMessage}`,
398
+ },
399
+ ],
400
+ }
401
+ }
402
+ }
403
+
404
+ server.tool(
405
+ 'createJob',
406
+ 'Creates a new Payload job (task or workflow) with specified configuration',
407
+ toolSchemas.createJob.parameters.shape,
408
+ async (args) => {
409
+ return tool(
410
+ args.jobName,
411
+ args.jobType,
412
+ args.jobSlug,
413
+ args.description,
414
+ args.inputSchema,
415
+ args.outputSchema,
416
+ args.jobData,
417
+ )
418
+ },
419
+ )
420
+ }