@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.
- package/LICENSE.md +22 -0
- package/README.md +7 -0
- package/dist/collections/createApiKeysCollection.d.ts +7 -0
- package/dist/collections/createApiKeysCollection.d.ts.map +1 -0
- package/dist/collections/createApiKeysCollection.js +315 -0
- package/dist/collections/createApiKeysCollection.js.map +1 -0
- package/dist/endpoints/mcp.d.ts +4 -0
- package/dist/endpoints/mcp.d.ts.map +1 -0
- package/dist/endpoints/mcp.js +44 -0
- package/dist/endpoints/mcp.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/createRequest.d.ts +3 -0
- package/dist/mcp/createRequest.d.ts.map +1 -0
- package/dist/mcp/createRequest.js +14 -0
- package/dist/mcp/createRequest.js.map +1 -0
- package/dist/mcp/getMcpHandler.d.ts +4 -0
- package/dist/mcp/getMcpHandler.d.ts.map +1 -0
- package/dist/mcp/getMcpHandler.js +179 -0
- package/dist/mcp/getMcpHandler.js.map +1 -0
- package/dist/mcp/helpers/config.d.ts +30 -0
- package/dist/mcp/helpers/config.d.ts.map +1 -0
- package/dist/mcp/helpers/config.js +217 -0
- package/dist/mcp/helpers/config.js.map +1 -0
- package/dist/mcp/helpers/conversion.d.ts +2 -0
- package/dist/mcp/helpers/conversion.d.ts.map +1 -0
- package/dist/mcp/helpers/conversion.js +5 -0
- package/dist/mcp/helpers/conversion.js.map +1 -0
- package/dist/mcp/helpers/fields.d.ts +38 -0
- package/dist/mcp/helpers/fields.d.ts.map +1 -0
- package/dist/mcp/helpers/fields.js +96 -0
- package/dist/mcp/helpers/fields.js.map +1 -0
- package/dist/mcp/helpers/fileValidation.d.ts +69 -0
- package/dist/mcp/helpers/fileValidation.d.ts.map +1 -0
- package/dist/mcp/helpers/fileValidation.js +305 -0
- package/dist/mcp/helpers/fileValidation.js.map +1 -0
- package/dist/mcp/helpers/validation.d.ts +9 -0
- package/dist/mcp/helpers/validation.d.ts.map +1 -0
- package/dist/mcp/helpers/validation.js +22 -0
- package/dist/mcp/helpers/validation.js.map +1 -0
- package/dist/mcp/registerTool.d.ts +6 -0
- package/dist/mcp/registerTool.d.ts.map +1 -0
- package/dist/mcp/registerTool.js +18 -0
- package/dist/mcp/registerTool.js.map +1 -0
- package/dist/mcp/tools/auth/auth.d.ts +4 -0
- package/dist/mcp/tools/auth/auth.d.ts.map +1 -0
- package/dist/mcp/tools/auth/auth.js +54 -0
- package/dist/mcp/tools/auth/auth.js.map +1 -0
- package/dist/mcp/tools/auth/forgotPassword.d.ts +4 -0
- package/dist/mcp/tools/auth/forgotPassword.d.ts.map +1 -0
- package/dist/mcp/tools/auth/forgotPassword.js +45 -0
- package/dist/mcp/tools/auth/forgotPassword.js.map +1 -0
- package/dist/mcp/tools/auth/login.d.ts +4 -0
- package/dist/mcp/tools/auth/login.d.ts.map +1 -0
- package/dist/mcp/tools/auth/login.js +48 -0
- package/dist/mcp/tools/auth/login.js.map +1 -0
- package/dist/mcp/tools/auth/resetPassword.d.ts +4 -0
- package/dist/mcp/tools/auth/resetPassword.d.ts.map +1 -0
- package/dist/mcp/tools/auth/resetPassword.js +46 -0
- package/dist/mcp/tools/auth/resetPassword.js.map +1 -0
- package/dist/mcp/tools/auth/unlock.d.ts +4 -0
- package/dist/mcp/tools/auth/unlock.d.ts.map +1 -0
- package/dist/mcp/tools/auth/unlock.js +45 -0
- package/dist/mcp/tools/auth/unlock.js.map +1 -0
- package/dist/mcp/tools/auth/verify.d.ts +4 -0
- package/dist/mcp/tools/auth/verify.d.ts.map +1 -0
- package/dist/mcp/tools/auth/verify.js +42 -0
- package/dist/mcp/tools/auth/verify.js.map +1 -0
- package/dist/mcp/tools/collection/create.d.ts +10 -0
- package/dist/mcp/tools/collection/create.d.ts.map +1 -0
- package/dist/mcp/tools/collection/create.js +159 -0
- package/dist/mcp/tools/collection/create.js.map +1 -0
- package/dist/mcp/tools/collection/delete.d.ts +10 -0
- package/dist/mcp/tools/collection/delete.d.ts.map +1 -0
- package/dist/mcp/tools/collection/delete.js +162 -0
- package/dist/mcp/tools/collection/delete.js.map +1 -0
- package/dist/mcp/tools/collection/find.d.ts +10 -0
- package/dist/mcp/tools/collection/find.d.ts.map +1 -0
- package/dist/mcp/tools/collection/find.js +162 -0
- package/dist/mcp/tools/collection/find.js.map +1 -0
- package/dist/mcp/tools/collection/update.d.ts +10 -0
- package/dist/mcp/tools/collection/update.d.ts.map +1 -0
- package/dist/mcp/tools/collection/update.js +206 -0
- package/dist/mcp/tools/collection/update.js.map +1 -0
- package/dist/mcp/tools/config/find.d.ts +10 -0
- package/dist/mcp/tools/config/find.d.ts.map +1 -0
- package/dist/mcp/tools/config/find.js +94 -0
- package/dist/mcp/tools/config/find.js.map +1 -0
- package/dist/mcp/tools/config/update.d.ts +10 -0
- package/dist/mcp/tools/config/update.d.ts.map +1 -0
- package/dist/mcp/tools/config/update.js +212 -0
- package/dist/mcp/tools/config/update.js.map +1 -0
- package/dist/mcp/tools/job/create.d.ts +10 -0
- package/dist/mcp/tools/job/create.d.ts.map +1 -0
- package/dist/mcp/tools/job/create.js +293 -0
- package/dist/mcp/tools/job/create.js.map +1 -0
- package/dist/mcp/tools/job/run.d.ts +10 -0
- package/dist/mcp/tools/job/run.d.ts.map +1 -0
- package/dist/mcp/tools/job/run.js +147 -0
- package/dist/mcp/tools/job/run.js.map +1 -0
- package/dist/mcp/tools/job/update.d.ts +11 -0
- package/dist/mcp/tools/job/update.d.ts.map +1 -0
- package/dist/mcp/tools/job/update.js +211 -0
- package/dist/mcp/tools/job/update.js.map +1 -0
- package/dist/mcp/tools/resource/create.d.ts +6 -0
- package/dist/mcp/tools/resource/create.d.ts.map +1 -0
- package/dist/mcp/tools/resource/create.js +75 -0
- package/dist/mcp/tools/resource/create.js.map +1 -0
- package/dist/mcp/tools/resource/delete.d.ts +5 -0
- package/dist/mcp/tools/resource/delete.d.ts.map +1 -0
- package/dist/mcp/tools/resource/delete.js +140 -0
- package/dist/mcp/tools/resource/delete.js.map +1 -0
- package/dist/mcp/tools/resource/find.d.ts +5 -0
- package/dist/mcp/tools/resource/find.d.ts.map +1 -0
- package/dist/mcp/tools/resource/find.js +119 -0
- package/dist/mcp/tools/resource/find.js.map +1 -0
- package/dist/mcp/tools/resource/update.d.ts +6 -0
- package/dist/mcp/tools/resource/update.d.ts.map +1 -0
- package/dist/mcp/tools/resource/update.js +201 -0
- package/dist/mcp/tools/resource/update.js.map +1 -0
- package/dist/mcp/tools/schemas.d.ts +374 -0
- package/dist/mcp/tools/schemas.d.ts.map +1 -0
- package/dist/mcp/tools/schemas.js +201 -0
- package/dist/mcp/tools/schemas.js.map +1 -0
- package/dist/types.d.ts +379 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/camelCase.d.ts +9 -0
- package/dist/utils/camelCase.d.ts.map +1 -0
- package/dist/utils/camelCase.js +11 -0
- package/dist/utils/camelCase.js.map +1 -0
- package/dist/utils/convertCollectionSchemaToZod.d.ts +3 -0
- package/dist/utils/convertCollectionSchemaToZod.d.ts.map +1 -0
- package/dist/utils/convertCollectionSchemaToZod.js +30 -0
- package/dist/utils/convertCollectionSchemaToZod.js.map +1 -0
- package/package.json +64 -0
- package/src/collections/createApiKeysCollection.ts +393 -0
- package/src/endpoints/mcp.ts +60 -0
- package/src/index.ts +86 -0
- package/src/mcp/createRequest.ts +13 -0
- package/src/mcp/getMcpHandler.ts +433 -0
- package/src/mcp/helpers/config.ts +326 -0
- package/src/mcp/helpers/conversion.ts +3 -0
- package/src/mcp/helpers/fields.ts +158 -0
- package/src/mcp/helpers/fileValidation.ts +417 -0
- package/src/mcp/helpers/validation.ts +32 -0
- package/src/mcp/registerTool.ts +22 -0
- package/src/mcp/tools/auth/auth.ts +69 -0
- package/src/mcp/tools/auth/forgotPassword.ts +68 -0
- package/src/mcp/tools/auth/login.ts +70 -0
- package/src/mcp/tools/auth/resetPassword.ts +59 -0
- package/src/mcp/tools/auth/unlock.ts +62 -0
- package/src/mcp/tools/auth/verify.ts +55 -0
- package/src/mcp/tools/collection/create.ts +236 -0
- package/src/mcp/tools/collection/delete.ts +227 -0
- package/src/mcp/tools/collection/find.ts +222 -0
- package/src/mcp/tools/collection/update.ts +288 -0
- package/src/mcp/tools/config/find.ts +126 -0
- package/src/mcp/tools/config/update.ts +282 -0
- package/src/mcp/tools/job/create.ts +420 -0
- package/src/mcp/tools/job/run.ts +189 -0
- package/src/mcp/tools/job/update.ts +319 -0
- package/src/mcp/tools/resource/create.ts +121 -0
- package/src/mcp/tools/resource/delete.ts +210 -0
- package/src/mcp/tools/resource/find.ts +194 -0
- package/src/mcp/tools/resource/update.ts +314 -0
- package/src/mcp/tools/schemas.ts +373 -0
- package/src/types.ts +405 -0
- package/src/utils/camelCase.ts +12 -0
- 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
|
+
}
|