suparank 1.2.5 → 1.2.7
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/bin/suparank.js +85 -536
- package/credentials.example.json +36 -18
- package/mcp-client/config.js +37 -0
- package/mcp-client/handlers/action.js +33 -0
- package/mcp-client/handlers/backend.js +43 -0
- package/mcp-client/handlers/index.js +9 -0
- package/mcp-client/handlers/orchestrator.js +850 -0
- package/mcp-client/index.js +33 -0
- package/mcp-client/publishers/ghost.js +105 -0
- package/mcp-client/publishers/image.js +306 -0
- package/mcp-client/publishers/index.js +20 -0
- package/mcp-client/publishers/webhook.js +76 -0
- package/mcp-client/publishers/wordpress.js +220 -0
- package/mcp-client/server.js +220 -0
- package/mcp-client/services/api.js +101 -0
- package/mcp-client/services/credentials.js +149 -0
- package/mcp-client/services/index.js +11 -0
- package/mcp-client/services/project.js +40 -0
- package/mcp-client/services/session-state.js +201 -0
- package/mcp-client/services/stats.js +50 -0
- package/mcp-client/tools/definitions.js +679 -0
- package/mcp-client/tools/discovery.js +132 -0
- package/mcp-client/tools/index.js +22 -0
- package/mcp-client/utils/content.js +126 -0
- package/mcp-client/utils/formatting.js +71 -0
- package/mcp-client/utils/index.js +10 -0
- package/mcp-client/utils/logging.js +38 -0
- package/mcp-client/utils/paths.js +134 -0
- package/mcp-client/workflow/index.js +10 -0
- package/mcp-client/workflow/planner.js +513 -0
- package/package.json +8 -19
- package/mcp-client.js +0 -3693
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Suparank MCP - Main Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Modular MCP server for AI-powered SEO content creation
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx suparank
|
|
10
|
+
* node mcp-client/index.js <project-slug> <api-key>
|
|
11
|
+
*
|
|
12
|
+
* Credentials:
|
|
13
|
+
* Local credentials are loaded from ~/.suparank/credentials.json
|
|
14
|
+
* These enable additional tools: image generation, CMS publishing, webhooks
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { main } from './server.js'
|
|
18
|
+
import { log } from './utils/logging.js'
|
|
19
|
+
|
|
20
|
+
// Re-export modules for external use
|
|
21
|
+
export * from './config.js'
|
|
22
|
+
export * from './utils/index.js'
|
|
23
|
+
export * from './services/index.js'
|
|
24
|
+
export * from './tools/index.js'
|
|
25
|
+
export * from './handlers/index.js'
|
|
26
|
+
export * from './publishers/index.js'
|
|
27
|
+
export * from './workflow/index.js'
|
|
28
|
+
|
|
29
|
+
// Run server
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
log('Fatal error:', error)
|
|
32
|
+
process.exit(1)
|
|
33
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suparank MCP - Ghost Publisher
|
|
3
|
+
*
|
|
4
|
+
* Publish content to Ghost CMS using Admin API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log, progress } from '../utils/logging.js'
|
|
8
|
+
import { fetchWithRetry } from '../services/api.js'
|
|
9
|
+
import { getCredentials } from '../services/credentials.js'
|
|
10
|
+
import { markdownToHtml } from '../utils/formatting.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create JWT for Ghost Admin API
|
|
14
|
+
* @param {string} id - API key ID
|
|
15
|
+
* @param {string} secret - API key secret
|
|
16
|
+
* @returns {Promise<string>} JWT token
|
|
17
|
+
*/
|
|
18
|
+
async function createGhostJWT(id, secret) {
|
|
19
|
+
// Simple JWT creation for Ghost
|
|
20
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: id })).toString('base64url')
|
|
21
|
+
const now = Math.floor(Date.now() / 1000)
|
|
22
|
+
const payload = Buffer.from(JSON.stringify({
|
|
23
|
+
iat: now,
|
|
24
|
+
exp: now + 300, // 5 minutes
|
|
25
|
+
aud: '/admin/'
|
|
26
|
+
})).toString('base64url')
|
|
27
|
+
|
|
28
|
+
// Create signature using crypto
|
|
29
|
+
const crypto = await import('crypto')
|
|
30
|
+
const key = Buffer.from(secret, 'hex')
|
|
31
|
+
const signature = crypto.createHmac('sha256', key)
|
|
32
|
+
.update(`${header}.${payload}`)
|
|
33
|
+
.digest('base64url')
|
|
34
|
+
|
|
35
|
+
return `${header}.${payload}.${signature}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Publish content to Ghost CMS
|
|
40
|
+
* @param {object} args - Publish arguments
|
|
41
|
+
* @param {string} args.title - Post title
|
|
42
|
+
* @param {string} args.content - Post content (markdown)
|
|
43
|
+
* @param {string} [args.status='draft'] - Publication status
|
|
44
|
+
* @param {string[]} [args.tags=[]] - Tag names
|
|
45
|
+
* @param {string} [args.featured_image_url] - Featured image URL
|
|
46
|
+
* @returns {Promise<object>} MCP response
|
|
47
|
+
*/
|
|
48
|
+
export async function executeGhostPublish(args) {
|
|
49
|
+
const credentials = getCredentials()
|
|
50
|
+
const { api_url, admin_api_key } = credentials.ghost
|
|
51
|
+
const { title, content, status = 'draft', tags = [], featured_image_url } = args
|
|
52
|
+
|
|
53
|
+
progress('Publish', `Publishing to Ghost: "${title}"`)
|
|
54
|
+
log(`Publishing to Ghost: ${title}`)
|
|
55
|
+
|
|
56
|
+
// Create JWT for Ghost Admin API
|
|
57
|
+
const [id, secret] = admin_api_key.split(':')
|
|
58
|
+
const token = await createGhostJWT(id, secret)
|
|
59
|
+
|
|
60
|
+
// Convert markdown to HTML for proper element separation
|
|
61
|
+
const htmlContent = markdownToHtml(content)
|
|
62
|
+
|
|
63
|
+
// Use HTML card for proper rendering (each element separate)
|
|
64
|
+
const mobiledoc = JSON.stringify({
|
|
65
|
+
version: '0.3.1',
|
|
66
|
+
atoms: [],
|
|
67
|
+
cards: [['html', { html: htmlContent }]],
|
|
68
|
+
markups: [],
|
|
69
|
+
sections: [[10, 0]]
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const postData = {
|
|
73
|
+
posts: [{
|
|
74
|
+
title,
|
|
75
|
+
mobiledoc,
|
|
76
|
+
status,
|
|
77
|
+
tags: tags.map(name => ({ name })),
|
|
78
|
+
feature_image: featured_image_url
|
|
79
|
+
}]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const response = await fetchWithRetry(`${api_url}/ghost/api/admin/posts/`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Authorization': `Ghost ${token}`,
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(postData)
|
|
89
|
+
}, 2, 30000) // 2 retries, 30s timeout
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const error = await response.text()
|
|
93
|
+
throw new Error(`Ghost error: ${error}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await response.json()
|
|
97
|
+
const post = result.posts[0]
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: `Post published to Ghost!\n\n**Title:** ${post.title}\n**Status:** ${post.status}\n**URL:** ${post.url}\n**ID:** ${post.id}\n\n${status === 'draft' ? 'The post is saved as a draft. Edit and publish from Ghost dashboard.' : 'The post is now live!'}`
|
|
103
|
+
}]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suparank MCP - Image Publisher
|
|
3
|
+
*
|
|
4
|
+
* AI image generation using fal.ai, Gemini, or Wiro
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log, progress } from '../utils/logging.js'
|
|
8
|
+
import { fetchWithRetry } from '../services/api.js'
|
|
9
|
+
import { getCredentials } from '../services/credentials.js'
|
|
10
|
+
import { sessionState, saveSession } from '../services/session-state.js'
|
|
11
|
+
import { incrementStat } from '../services/stats.js'
|
|
12
|
+
import { API_ENDPOINTS } from '../config.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate an AI image using the configured provider
|
|
16
|
+
* @param {object} args - Image generation arguments
|
|
17
|
+
* @param {string} args.prompt - Image prompt
|
|
18
|
+
* @param {string} [args.style] - Style guidance
|
|
19
|
+
* @param {string} [args.aspect_ratio='16:9'] - Aspect ratio
|
|
20
|
+
* @returns {Promise<object>} MCP response with image URL
|
|
21
|
+
*/
|
|
22
|
+
export async function executeImageGeneration(args) {
|
|
23
|
+
const credentials = getCredentials()
|
|
24
|
+
const provider = credentials.image_provider
|
|
25
|
+
const config = credentials[provider]
|
|
26
|
+
|
|
27
|
+
if (!config?.api_key) {
|
|
28
|
+
throw new Error(`${provider} API key not configured`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
progress('Image', `Generating with ${provider}...`)
|
|
32
|
+
|
|
33
|
+
const { prompt, style, aspect_ratio = '16:9' } = args
|
|
34
|
+
const fullPrompt = style ? `${prompt}, ${style}` : prompt
|
|
35
|
+
|
|
36
|
+
log(`Generating image with ${provider}: ${fullPrompt.substring(0, 50)}...`)
|
|
37
|
+
|
|
38
|
+
switch (provider) {
|
|
39
|
+
case 'fal':
|
|
40
|
+
return generateWithFal(config, fullPrompt, aspect_ratio)
|
|
41
|
+
|
|
42
|
+
case 'gemini':
|
|
43
|
+
return generateWithGemini(config, fullPrompt, aspect_ratio)
|
|
44
|
+
|
|
45
|
+
case 'wiro':
|
|
46
|
+
return generateWithWiro(config, fullPrompt, aspect_ratio)
|
|
47
|
+
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`Unknown image provider: ${provider}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate image with fal.ai
|
|
55
|
+
*/
|
|
56
|
+
async function generateWithFal(config, fullPrompt, aspect_ratio) {
|
|
57
|
+
const response = await fetchWithRetry(API_ENDPOINTS.fal, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Authorization': `Key ${config.api_key}`,
|
|
61
|
+
'Content-Type': 'application/json'
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
prompt: fullPrompt,
|
|
65
|
+
aspect_ratio: aspect_ratio,
|
|
66
|
+
output_format: 'png',
|
|
67
|
+
resolution: '1K',
|
|
68
|
+
num_images: 1
|
|
69
|
+
})
|
|
70
|
+
}, 2, 60000) // 2 retries, 60s timeout for image generation
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const error = await response.text()
|
|
74
|
+
throw new Error(`fal.ai error: ${error}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await response.json()
|
|
78
|
+
const imageUrl = result.images?.[0]?.url
|
|
79
|
+
|
|
80
|
+
// Store in session for orchestrated workflows
|
|
81
|
+
storeImageInSession(imageUrl)
|
|
82
|
+
|
|
83
|
+
// Track stats
|
|
84
|
+
incrementStat('images_generated')
|
|
85
|
+
|
|
86
|
+
const imageNumber = 1 + sessionState.inlineImages.length
|
|
87
|
+
const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
|
|
88
|
+
const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
content: [{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: `# ✅ ${imageType} Generated (${imageNumber}/${totalImages})
|
|
94
|
+
|
|
95
|
+
**URL:** ${imageUrl}
|
|
96
|
+
|
|
97
|
+
**Prompt:** ${fullPrompt}
|
|
98
|
+
**Provider:** fal.ai (nano-banana-pro)
|
|
99
|
+
**Aspect Ratio:** ${aspect_ratio}
|
|
100
|
+
|
|
101
|
+
${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber} more image(s).` : '\n**All images generated!** Proceed to publish.'}`
|
|
102
|
+
}]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate image with Google Gemini
|
|
108
|
+
*/
|
|
109
|
+
async function generateWithGemini(config, fullPrompt, aspect_ratio) {
|
|
110
|
+
const model = config.model || 'gemini-3-pro-image-preview'
|
|
111
|
+
const response = await fetch(
|
|
112
|
+
`${API_ENDPOINTS.gemini}/${model}:generateContent`,
|
|
113
|
+
{
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
'x-goog-api-key': config.api_key
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
contents: [{
|
|
121
|
+
parts: [{ text: fullPrompt }]
|
|
122
|
+
}],
|
|
123
|
+
generationConfig: {
|
|
124
|
+
responseModalities: ['IMAGE'],
|
|
125
|
+
imageConfig: {
|
|
126
|
+
aspectRatio: aspect_ratio,
|
|
127
|
+
imageSize: '1K'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const error = await response.text()
|
|
136
|
+
throw new Error(`Gemini error: ${error}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = await response.json()
|
|
140
|
+
const imagePart = result.candidates?.[0]?.content?.parts?.find(p => p.inlineData)
|
|
141
|
+
const imageData = imagePart?.inlineData?.data
|
|
142
|
+
const mimeType = imagePart?.inlineData?.mimeType || 'image/png'
|
|
143
|
+
|
|
144
|
+
if (!imageData) {
|
|
145
|
+
throw new Error('No image data in Gemini response')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Return base64 data URI
|
|
149
|
+
const dataUri = `data:${mimeType};base64,${imageData}`
|
|
150
|
+
|
|
151
|
+
// Track stats
|
|
152
|
+
incrementStat('images_generated')
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
content: [{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: `Image generated successfully!\n\n**Format:** Base64 Data URI\n**Prompt:** ${fullPrompt}\n**Provider:** Google Gemini (${model})\n**Aspect Ratio:** ${aspect_ratio}\n\n**Data URI:** ${dataUri.substring(0, 100)}...\n\n[Full base64 data: ${imageData.length} chars]`
|
|
158
|
+
}]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generate image with Wiro
|
|
164
|
+
*/
|
|
165
|
+
async function generateWithWiro(config, fullPrompt, aspect_ratio) {
|
|
166
|
+
const crypto = await import('crypto')
|
|
167
|
+
const apiKey = config.api_key
|
|
168
|
+
const apiSecret = config.api_secret
|
|
169
|
+
|
|
170
|
+
if (!apiSecret) {
|
|
171
|
+
throw new Error('Wiro API secret not configured. Add api_secret to wiro config in ~/.suparank/credentials.json')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Generate nonce and signature
|
|
175
|
+
const nonce = Math.floor(Date.now() / 1000).toString()
|
|
176
|
+
const signatureData = `${apiSecret}${nonce}`
|
|
177
|
+
const signature = crypto.createHmac('sha256', apiKey)
|
|
178
|
+
.update(signatureData)
|
|
179
|
+
.digest('hex')
|
|
180
|
+
|
|
181
|
+
const model = config.model || 'google/nano-banana-pro'
|
|
182
|
+
|
|
183
|
+
// Submit task
|
|
184
|
+
log(`Submitting wiro.ai task for model: ${model}`)
|
|
185
|
+
const submitResponse = await fetch(`${API_ENDPOINTS.wiro}/Run/${model}`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: {
|
|
188
|
+
'Content-Type': 'application/json',
|
|
189
|
+
'x-api-key': apiKey,
|
|
190
|
+
'x-nonce': nonce,
|
|
191
|
+
'x-signature': signature
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
prompt: fullPrompt,
|
|
195
|
+
aspectRatio: aspect_ratio,
|
|
196
|
+
resolution: '1K',
|
|
197
|
+
safetySetting: 'BLOCK_ONLY_HIGH'
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (!submitResponse.ok) {
|
|
202
|
+
const error = await submitResponse.text()
|
|
203
|
+
throw new Error(`wiro.ai submit error: ${error}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const submitResult = await submitResponse.json()
|
|
207
|
+
if (!submitResult.result || !submitResult.taskid) {
|
|
208
|
+
throw new Error(`wiro.ai task submission failed: ${JSON.stringify(submitResult.errors)}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const taskId = submitResult.taskid
|
|
212
|
+
log(`wiro.ai task submitted: ${taskId}`)
|
|
213
|
+
|
|
214
|
+
// Poll for completion
|
|
215
|
+
const maxAttempts = 60 // 60 seconds max
|
|
216
|
+
const pollInterval = 2000 // 2 seconds
|
|
217
|
+
|
|
218
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
219
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
220
|
+
|
|
221
|
+
// Generate new signature for poll request
|
|
222
|
+
const pollNonce = Math.floor(Date.now() / 1000).toString()
|
|
223
|
+
const pollSignatureData = `${apiSecret}${pollNonce}`
|
|
224
|
+
const pollSignature = crypto.createHmac('sha256', apiKey)
|
|
225
|
+
.update(pollSignatureData)
|
|
226
|
+
.digest('hex')
|
|
227
|
+
|
|
228
|
+
const pollResponse = await fetch(API_ENDPOINTS.wiroTaskDetail, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
'x-api-key': apiKey,
|
|
233
|
+
'x-nonce': pollNonce,
|
|
234
|
+
'x-signature': pollSignature
|
|
235
|
+
},
|
|
236
|
+
body: JSON.stringify({ taskid: taskId })
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
if (!pollResponse.ok) {
|
|
240
|
+
log(`wiro.ai poll error: ${await pollResponse.text()}`)
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const pollResult = await pollResponse.json()
|
|
245
|
+
const task = pollResult.tasklist?.[0]
|
|
246
|
+
|
|
247
|
+
if (!task) continue
|
|
248
|
+
|
|
249
|
+
const status = task.status
|
|
250
|
+
log(`wiro.ai task status: ${status}`)
|
|
251
|
+
|
|
252
|
+
// Check for completion
|
|
253
|
+
if (status === 'task_postprocess_end') {
|
|
254
|
+
const imageUrl = task.outputs?.[0]?.url
|
|
255
|
+
if (!imageUrl) {
|
|
256
|
+
throw new Error('wiro.ai task completed but no output URL')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Store in session for orchestrated workflows
|
|
260
|
+
storeImageInSession(imageUrl)
|
|
261
|
+
|
|
262
|
+
// Track stats
|
|
263
|
+
incrementStat('images_generated')
|
|
264
|
+
|
|
265
|
+
const imageNumber = 1 + sessionState.inlineImages.length
|
|
266
|
+
const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
|
|
267
|
+
const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
content: [{
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: `# ✅ ${imageType} Generated (${imageNumber}/${totalImages})
|
|
273
|
+
|
|
274
|
+
**URL:** ${imageUrl}
|
|
275
|
+
|
|
276
|
+
**Prompt:** ${fullPrompt}
|
|
277
|
+
**Provider:** wiro.ai (${model})
|
|
278
|
+
**Aspect Ratio:** ${aspect_ratio}
|
|
279
|
+
**Processing Time:** ${task.elapsedseconds}s
|
|
280
|
+
|
|
281
|
+
${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber} more image(s).` : '\n**All images generated!** Proceed to publish.'}`
|
|
282
|
+
}]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for failure
|
|
287
|
+
if (status === 'task_cancel') {
|
|
288
|
+
throw new Error('wiro.ai task was cancelled')
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw new Error('wiro.ai task timed out after 60 seconds')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Store generated image URL in session
|
|
297
|
+
* First image is cover, subsequent are inline
|
|
298
|
+
*/
|
|
299
|
+
function storeImageInSession(imageUrl) {
|
|
300
|
+
if (!sessionState.imageUrl) {
|
|
301
|
+
sessionState.imageUrl = imageUrl
|
|
302
|
+
} else {
|
|
303
|
+
sessionState.inlineImages.push(imageUrl)
|
|
304
|
+
}
|
|
305
|
+
saveSession()
|
|
306
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suparank MCP - Publishers Module
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all publisher functions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Image generation
|
|
8
|
+
export { executeImageGeneration } from './image.js'
|
|
9
|
+
|
|
10
|
+
// WordPress publishing
|
|
11
|
+
export {
|
|
12
|
+
executeWordPressPublish,
|
|
13
|
+
fetchWordPressCategories
|
|
14
|
+
} from './wordpress.js'
|
|
15
|
+
|
|
16
|
+
// Ghost publishing
|
|
17
|
+
export { executeGhostPublish } from './ghost.js'
|
|
18
|
+
|
|
19
|
+
// Webhook sending
|
|
20
|
+
export { executeSendWebhook } from './webhook.js'
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suparank MCP - Webhook Publisher
|
|
3
|
+
*
|
|
4
|
+
* Send data to configured webhooks (Make.com, n8n, Zapier, Slack)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log } from '../utils/logging.js'
|
|
8
|
+
import { getCredentials } from '../services/credentials.js'
|
|
9
|
+
import { projectSlug } from '../config.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Send data to a webhook
|
|
13
|
+
* @param {object} args - Webhook arguments
|
|
14
|
+
* @param {string} [args.webhook_type='default'] - Webhook type
|
|
15
|
+
* @param {object} [args.payload={}] - Data payload
|
|
16
|
+
* @param {string} [args.message] - Message text (for Slack)
|
|
17
|
+
* @returns {Promise<object>} MCP response
|
|
18
|
+
*/
|
|
19
|
+
export async function executeSendWebhook(args) {
|
|
20
|
+
const credentials = getCredentials()
|
|
21
|
+
const { webhook_type = 'default', payload = {}, message } = args
|
|
22
|
+
const webhooks = credentials.webhooks
|
|
23
|
+
|
|
24
|
+
// Get webhook URL
|
|
25
|
+
const urlMap = {
|
|
26
|
+
default: webhooks.default_url,
|
|
27
|
+
make: webhooks.make_url,
|
|
28
|
+
n8n: webhooks.n8n_url,
|
|
29
|
+
zapier: webhooks.zapier_url,
|
|
30
|
+
slack: webhooks.slack_url
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const url = urlMap[webhook_type]
|
|
34
|
+
if (!url) {
|
|
35
|
+
throw new Error(`No ${webhook_type} webhook URL configured`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log(`Sending webhook to ${webhook_type}: ${url}`)
|
|
39
|
+
|
|
40
|
+
// Format payload based on type
|
|
41
|
+
let body
|
|
42
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
43
|
+
|
|
44
|
+
if (webhook_type === 'slack') {
|
|
45
|
+
body = JSON.stringify({
|
|
46
|
+
text: message || 'Message from Writer MCP',
|
|
47
|
+
...payload
|
|
48
|
+
})
|
|
49
|
+
} else {
|
|
50
|
+
body = JSON.stringify({
|
|
51
|
+
source: 'suparank',
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
project: projectSlug,
|
|
54
|
+
data: payload,
|
|
55
|
+
message
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers,
|
|
62
|
+
body
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const error = await response.text()
|
|
67
|
+
throw new Error(`Webhook error (${response.status}): ${error}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: `Webhook sent successfully!\n\n**Type:** ${webhook_type}\n**URL:** ${url.substring(0, 50)}...\n**Status:** ${response.status}\n\nThe data has been sent to your ${webhook_type} webhook.`
|
|
74
|
+
}]
|
|
75
|
+
}
|
|
76
|
+
}
|