openhive-mcp-server 1.0.0 → 1.2.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/index.js +600 -32
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* OpenHive MCP CLI
|
|
5
5
|
*
|
|
6
6
|
* Connects IDEs (Antigravity, Claude Desktop, Cursor, etc.) to your OpenHive instance.
|
|
7
|
+
* Supports single image posts and carousels (2-10 images).
|
|
7
8
|
*
|
|
8
9
|
* Environment variables:
|
|
9
10
|
* OPENHIVE_API_URL - Your OpenHive API URL (e.g., https://your-server.com)
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* "mcpServers": {
|
|
15
16
|
* "openhive": {
|
|
16
17
|
* "command": "npx",
|
|
17
|
-
* "args": ["-y", "openhive-mcp"],
|
|
18
|
+
* "args": ["-y", "openhive-mcp-server"],
|
|
18
19
|
* "env": {
|
|
19
20
|
* "OPENHIVE_API_URL": "https://your-api-url",
|
|
20
21
|
* "OPENHIVE_API_TOKEN": "your-token"
|
|
@@ -48,23 +49,170 @@ async function apiRequest(path, options = {}) {
|
|
|
48
49
|
},
|
|
49
50
|
});
|
|
50
51
|
const data = await res.json();
|
|
51
|
-
if (!res.ok) throw new Error(data.error ||
|
|
52
|
+
if (!res.ok) throw new Error(data.error || `API request failed: ${res.status}`);
|
|
52
53
|
return data.data;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const server = new McpServer({ name: 'openhive', version: '1.
|
|
56
|
+
const server = new McpServer({ name: 'openhive', version: '1.2.0' });
|
|
56
57
|
|
|
57
58
|
// ── Posts ──
|
|
58
59
|
|
|
59
|
-
server.tool(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
server.tool(
|
|
61
|
+
'create_post',
|
|
62
|
+
'Cria um post para Instagram. Suporta imagem unica ou carrossel (2-10 imagens). Para carrossel, use image_prompts (gerar via IA) ou image_urls (URLs prontas)',
|
|
63
|
+
{
|
|
64
|
+
caption: z.string().optional().describe('Legenda do post'),
|
|
65
|
+
image_prompt: z.string().optional().describe('Prompt para gerar UMA imagem via IA'),
|
|
66
|
+
image_prompts: z.array(z.string()).min(2).max(10).optional().describe('Array de prompts para gerar carrossel (2-10 imagens via IA)'),
|
|
67
|
+
image_urls: z.array(z.string()).min(2).max(10).optional().describe('Array de URLs de imagens prontas para carrossel (2-10). Use com render_html_to_image'),
|
|
68
|
+
aspect_ratio: z.string().optional().describe('Formato: 1:1 (Feed), 4:5 (Retrato), 9:16 (Stories)'),
|
|
69
|
+
scheduled_at: z.string().optional().describe('Data/hora para agendar (ISO 8601)'),
|
|
70
|
+
hashtags: z.array(z.string()).optional().describe('Lista de hashtags'),
|
|
71
|
+
tone: z.string().optional().describe('Tom da legenda: educativo, inspirador, humor, noticia'),
|
|
72
|
+
},
|
|
73
|
+
async (input) => {
|
|
74
|
+
let imageUrl;
|
|
75
|
+
let images = [];
|
|
76
|
+
let caption = input.caption;
|
|
77
|
+
let hashtags = input.hashtags;
|
|
78
|
+
const aspectRatio = input.aspect_ratio || '1:1';
|
|
79
|
+
|
|
80
|
+
// Generate multiple images for carousel via IA
|
|
81
|
+
if (input.image_prompts && input.image_prompts.length >= 2) {
|
|
82
|
+
const results = await Promise.allSettled(
|
|
83
|
+
input.image_prompts.map((prompt) =>
|
|
84
|
+
apiRequest('/api/generate/image', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: JSON.stringify({ prompt, aspectRatio }),
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
images = results
|
|
91
|
+
.filter((r) => r.status === 'fulfilled')
|
|
92
|
+
.map((r, idx) => ({
|
|
93
|
+
imageUrl: r.value.imageUrl,
|
|
94
|
+
order: idx,
|
|
95
|
+
prompt: input.image_prompts[idx],
|
|
96
|
+
source: 'NANOBANA',
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const failed = results.filter((r) => r.status === 'rejected').length;
|
|
100
|
+
if (failed > 0) {
|
|
101
|
+
console.error(`[create_post] ${failed} image(s) failed to generate`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Use provided URLs directly for carousel
|
|
105
|
+
else if (input.image_urls && input.image_urls.length >= 2) {
|
|
106
|
+
images = input.image_urls.map((url, idx) => ({
|
|
107
|
+
imageUrl: url,
|
|
108
|
+
order: idx,
|
|
109
|
+
source: 'URL',
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
// Single image generation
|
|
113
|
+
else if (input.image_prompt) {
|
|
114
|
+
const img = await apiRequest('/api/generate/image', {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
body: JSON.stringify({ prompt: input.image_prompt, aspectRatio }),
|
|
117
|
+
});
|
|
118
|
+
imageUrl = img.imageUrl;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Auto-generate caption if not provided
|
|
122
|
+
if (!caption) {
|
|
123
|
+
const topic = input.image_prompt || (input.image_prompts && input.image_prompts[0]) || 'post de tecnologia';
|
|
124
|
+
const result = await apiRequest('/api/generate/caption', {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
body: JSON.stringify({ topic, tone: input.tone }),
|
|
127
|
+
});
|
|
128
|
+
caption = result.caption;
|
|
129
|
+
hashtags = hashtags || result.hashtags;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isCarousel = images.length >= 2;
|
|
133
|
+
|
|
134
|
+
const postBody = {
|
|
135
|
+
caption,
|
|
136
|
+
hashtags,
|
|
137
|
+
source: 'MCP',
|
|
138
|
+
aspectRatio,
|
|
139
|
+
isCarousel,
|
|
140
|
+
...(isCarousel ? { images } : { imageUrl }),
|
|
141
|
+
...(input.scheduled_at ? { scheduledAt: input.scheduled_at } : {}),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const post = await apiRequest('/api/posts', {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
body: JSON.stringify(postBody),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
content: [{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: JSON.stringify({
|
|
153
|
+
post_id: post.id,
|
|
154
|
+
caption: post.caption,
|
|
155
|
+
image_url: post.imageUrl,
|
|
156
|
+
is_carousel: post.isCarousel,
|
|
157
|
+
image_count: isCarousel ? images.length : (post.imageUrl ? 1 : 0),
|
|
158
|
+
images: post.images || [],
|
|
159
|
+
status: post.status,
|
|
160
|
+
}, null, 2),
|
|
161
|
+
}],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
server.tool(
|
|
167
|
+
'add_image_to_post',
|
|
168
|
+
'Adiciona uma imagem a um post existente. Se o post ficar com 2+ imagens, vira carrossel automaticamente',
|
|
169
|
+
{
|
|
170
|
+
post_id: z.string().describe('ID do post'),
|
|
171
|
+
image_url: z.string().optional().describe('URL de imagem pronta (de render_html_to_image ou upload)'),
|
|
172
|
+
image_prompt: z.string().optional().describe('Prompt para gerar imagem via IA'),
|
|
173
|
+
aspect_ratio: z.string().optional().describe('Formato: 1:1, 4:5, 9:16 (usado se gerar via IA)'),
|
|
174
|
+
},
|
|
175
|
+
async (input) => {
|
|
176
|
+
let imageUrl = input.image_url;
|
|
177
|
+
|
|
178
|
+
// Generate image if prompt provided
|
|
179
|
+
if (!imageUrl && input.image_prompt) {
|
|
180
|
+
const img = await apiRequest('/api/generate/image', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
prompt: input.image_prompt,
|
|
184
|
+
aspectRatio: input.aspect_ratio || '1:1',
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
imageUrl = img.imageUrl;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!imageUrl) {
|
|
191
|
+
return {
|
|
192
|
+
content: [{
|
|
193
|
+
type: 'text',
|
|
194
|
+
text: JSON.stringify({ error: 'Provide image_url or image_prompt' }),
|
|
195
|
+
}],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const result = await apiRequest(`/api/posts/${input.post_id}/images`, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
imageUrl,
|
|
203
|
+
source: input.image_prompt ? 'NANOBANA' : 'URL',
|
|
204
|
+
prompt: input.image_prompt || null,
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: JSON.stringify(result, null, 2),
|
|
212
|
+
}],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
);
|
|
68
216
|
|
|
69
217
|
server.tool('list_posts', 'Lista posts com filtros', {
|
|
70
218
|
status: z.string().optional().describe('Filtrar por status: DRAFT, SCHEDULED, PUBLISHED, FAILED'),
|
|
@@ -81,8 +229,13 @@ server.tool('list_posts', 'Lista posts com filtros', {
|
|
|
81
229
|
|
|
82
230
|
server.tool('publish_now', 'Publica um post imediatamente no Instagram', {
|
|
83
231
|
post_id: z.string().describe('ID do post'),
|
|
232
|
+
account_id: z.string().optional().describe('ID da conta Instagram (opcional, usa a padrao se nao informado)'),
|
|
84
233
|
}, async (input) => {
|
|
85
|
-
const
|
|
234
|
+
const body = input.account_id ? { accountId: input.account_id } : {};
|
|
235
|
+
const result = await apiRequest(`/api/posts/${input.post_id}/publish`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
body: JSON.stringify(body),
|
|
238
|
+
});
|
|
86
239
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
87
240
|
});
|
|
88
241
|
|
|
@@ -96,8 +249,8 @@ server.tool('schedule_post', 'Agenda um post para publicacao', {
|
|
|
96
249
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
97
250
|
});
|
|
98
251
|
|
|
99
|
-
server.tool('generate_image', 'Gera imagem
|
|
100
|
-
prompt: z.string().describe('Descricao da imagem'),
|
|
252
|
+
server.tool('generate_image', 'Gera uma imagem via IA (NanoBana)', {
|
|
253
|
+
prompt: z.string().describe('Descricao da imagem desejada'),
|
|
101
254
|
aspectRatio: z.string().optional().describe('Formato: 1:1, 4:5, 9:16'),
|
|
102
255
|
}, async (input) => {
|
|
103
256
|
const result = await apiRequest('/api/generate/image', {
|
|
@@ -106,7 +259,7 @@ server.tool('generate_image', 'Gera imagem com IA (Gemini)', {
|
|
|
106
259
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
107
260
|
});
|
|
108
261
|
|
|
109
|
-
server.tool('generate_caption', 'Gera legenda
|
|
262
|
+
server.tool('generate_caption', 'Gera legenda otimizada para Instagram', {
|
|
110
263
|
topic: z.string().describe('Tema do post'),
|
|
111
264
|
tone: z.string().optional().describe('Tom: educativo, inspirador, humor, noticia'),
|
|
112
265
|
}, async (input) => {
|
|
@@ -116,16 +269,31 @@ server.tool('generate_caption', 'Gera legenda com IA', {
|
|
|
116
269
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
117
270
|
});
|
|
118
271
|
|
|
119
|
-
server.tool(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
272
|
+
server.tool(
|
|
273
|
+
'render_html_to_image',
|
|
274
|
+
'Renderiza HTML/CSS/Tailwind em imagem PNG. Retorna image_url. Para carrossel: chame para cada slide e depois use create_post com image_urls contendo todas as URLs',
|
|
275
|
+
{
|
|
276
|
+
html: z.string().describe('Codigo HTML completo do slide (suporta Tailwind CSS via CDN)'),
|
|
277
|
+
width: z.number().optional().describe('Largura em pixels (default: 1080)'),
|
|
278
|
+
height: z.number().optional().describe('Altura em pixels (default: 1080). Use 1350 para 4:5, 1920 para 9:16'),
|
|
279
|
+
},
|
|
280
|
+
async (input) => {
|
|
281
|
+
const result = await apiRequest('/api/generate/html', {
|
|
282
|
+
method: 'POST', body: JSON.stringify(input),
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
content: [{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: JSON.stringify({
|
|
288
|
+
image_url: result.imageUrl,
|
|
289
|
+
width: input.width || 1080,
|
|
290
|
+
height: input.height || 1080,
|
|
291
|
+
tip: 'Colete todas as image_urls dos slides e use create_post({ image_urls: [...], caption: "..." }) para criar o carrossel',
|
|
292
|
+
}, null, 2),
|
|
293
|
+
}],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
);
|
|
129
297
|
|
|
130
298
|
server.tool('generate_template_image', 'Gera imagem usando template HTML pre-definido', {
|
|
131
299
|
title: z.string().describe('Texto principal'),
|
|
@@ -143,25 +311,61 @@ server.tool('generate_template_image', 'Gera imagem usando template HTML pre-def
|
|
|
143
311
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
144
312
|
});
|
|
145
313
|
|
|
314
|
+
server.tool('upload_image', 'Faz upload de uma imagem (base64) para o storage', {
|
|
315
|
+
image_base64: z.string().describe('Imagem em base64'),
|
|
316
|
+
filename: z.string().describe('Nome do arquivo (ex: foto.png)'),
|
|
317
|
+
}, async (input) => {
|
|
318
|
+
const result = await apiRequest('/api/upload', {
|
|
319
|
+
method: 'POST',
|
|
320
|
+
body: JSON.stringify(input),
|
|
321
|
+
});
|
|
322
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
server.tool('get_analytics', 'Retorna metricas dos posts publicados', {
|
|
326
|
+
period: z.string().optional().describe('Periodo: 7d, 30d ou 90d'),
|
|
327
|
+
}, async (input) => {
|
|
328
|
+
const params = new URLSearchParams();
|
|
329
|
+
if (input.period) params.set('period', input.period);
|
|
330
|
+
const result = await apiRequest(`/api/analytics?${params}`);
|
|
331
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
332
|
+
});
|
|
333
|
+
|
|
146
334
|
// ── Tasks ──
|
|
147
335
|
|
|
148
|
-
server.tool('create_task', 'Cria uma tarefa', {
|
|
336
|
+
server.tool('create_task', 'Cria uma tarefa de producao de conteudo', {
|
|
149
337
|
title: z.string().describe('Titulo da tarefa'),
|
|
150
338
|
description: z.string().optional().describe('Descricao'),
|
|
151
339
|
platform: z.string().optional().describe('Plataforma: YOUTUBE, INSTAGRAM, TIKTOK, OTHER'),
|
|
152
340
|
priority: z.string().optional().describe('Prioridade: LOW, MEDIUM, HIGH, URGENT'),
|
|
341
|
+
recordDate: z.string().optional().describe('Data de gravacao ISO'),
|
|
342
|
+
publishDate: z.string().optional().describe('Data de publicacao ISO'),
|
|
343
|
+
script: z.string().optional().describe('Roteiro'),
|
|
344
|
+
projectId: z.string().optional().describe('ID do projeto associado'),
|
|
345
|
+
isSponsored: z.boolean().optional().describe('Se e patrocinado'),
|
|
346
|
+
sponsorName: z.string().optional().describe('Nome do patrocinador'),
|
|
347
|
+
sponsorBriefing: z.string().optional().describe('Briefing do patrocinador'),
|
|
348
|
+
sponsorDeadline: z.string().optional().describe('Deadline do patrocinador ISO'),
|
|
153
349
|
}, async (input) => {
|
|
154
350
|
const result = await apiRequest('/api/tasks', { method: 'POST', body: JSON.stringify(input) });
|
|
155
351
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
156
352
|
});
|
|
157
353
|
|
|
158
|
-
server.tool('list_tasks', 'Lista tarefas', {
|
|
354
|
+
server.tool('list_tasks', 'Lista tarefas com filtros', {
|
|
159
355
|
status: z.string().optional().describe('Status: PENDING, IN_PROGRESS, COMPLETED, CANCELLED'),
|
|
356
|
+
priority: z.string().optional().describe('Prioridade: LOW, MEDIUM, HIGH, URGENT'),
|
|
357
|
+
platform: z.string().optional().describe('Plataforma: YOUTUBE, INSTAGRAM, TIKTOK, OTHER'),
|
|
358
|
+
projectId: z.string().optional().describe('ID do projeto'),
|
|
160
359
|
limit: z.string().optional().describe('Limite'),
|
|
360
|
+
page: z.string().optional().describe('Pagina'),
|
|
161
361
|
}, async (input) => {
|
|
162
362
|
const params = new URLSearchParams();
|
|
163
363
|
if (input.status) params.set('status', input.status);
|
|
364
|
+
if (input.priority) params.set('priority', input.priority);
|
|
365
|
+
if (input.platform) params.set('platform', input.platform);
|
|
366
|
+
if (input.projectId) params.set('projectId', input.projectId);
|
|
164
367
|
if (input.limit) params.set('limit', input.limit);
|
|
368
|
+
if (input.page) params.set('page', input.page);
|
|
165
369
|
const result = await apiRequest(`/api/tasks?${params}`);
|
|
166
370
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
167
371
|
});
|
|
@@ -169,8 +373,18 @@ server.tool('list_tasks', 'Lista tarefas', {
|
|
|
169
373
|
server.tool('update_task', 'Atualiza uma tarefa', {
|
|
170
374
|
task_id: z.string().describe('ID da tarefa'),
|
|
171
375
|
title: z.string().optional(),
|
|
172
|
-
|
|
173
|
-
|
|
376
|
+
description: z.string().optional(),
|
|
377
|
+
status: z.string().optional().describe('PENDING, IN_PROGRESS, COMPLETED, CANCELLED'),
|
|
378
|
+
priority: z.string().optional().describe('LOW, MEDIUM, HIGH, URGENT'),
|
|
379
|
+
platform: z.string().optional(),
|
|
380
|
+
recordDate: z.string().optional(),
|
|
381
|
+
publishDate: z.string().optional(),
|
|
382
|
+
script: z.string().optional(),
|
|
383
|
+
projectId: z.string().optional(),
|
|
384
|
+
isSponsored: z.boolean().optional(),
|
|
385
|
+
sponsorName: z.string().optional(),
|
|
386
|
+
sponsorBriefing: z.string().optional(),
|
|
387
|
+
sponsorDeadline: z.string().optional(),
|
|
174
388
|
}, async (input) => {
|
|
175
389
|
const { task_id, ...body } = input;
|
|
176
390
|
const result = await apiRequest(`/api/tasks/${task_id}`, { method: 'PUT', body: JSON.stringify(body) });
|
|
@@ -186,30 +400,384 @@ server.tool('delete_task', 'Remove uma tarefa', {
|
|
|
186
400
|
|
|
187
401
|
// ── Projects ──
|
|
188
402
|
|
|
189
|
-
server.tool('create_project', 'Cria um projeto', {
|
|
403
|
+
server.tool('create_project', 'Cria um projeto com modulos opcionais', {
|
|
190
404
|
title: z.string().describe('Titulo'),
|
|
191
405
|
description: z.string().optional(),
|
|
406
|
+
modules: z.array(z.object({
|
|
407
|
+
title: z.string(),
|
|
408
|
+
content: z.string().optional(),
|
|
409
|
+
})).optional().describe('Lista de modulos iniciais'),
|
|
192
410
|
}, async (input) => {
|
|
193
411
|
const result = await apiRequest('/api/projects', { method: 'POST', body: JSON.stringify(input) });
|
|
194
412
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
195
413
|
});
|
|
196
414
|
|
|
197
415
|
server.tool('list_projects', 'Lista projetos', {
|
|
416
|
+
status: z.string().optional().describe('PLANNING, IN_PROGRESS, COMPLETED, ARCHIVED'),
|
|
198
417
|
limit: z.string().optional(),
|
|
199
418
|
}, async (input) => {
|
|
200
419
|
const params = new URLSearchParams();
|
|
420
|
+
if (input.status) params.set('status', input.status);
|
|
201
421
|
if (input.limit) params.set('limit', input.limit);
|
|
202
422
|
const result = await apiRequest(`/api/projects?${params}`);
|
|
203
423
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
204
424
|
});
|
|
205
425
|
|
|
206
|
-
server.tool('get_project', 'Detalhes de um projeto', {
|
|
426
|
+
server.tool('get_project', 'Detalhes de um projeto com modulos e tarefas', {
|
|
207
427
|
project_id: z.string().describe('ID do projeto'),
|
|
208
428
|
}, async (input) => {
|
|
209
429
|
const result = await apiRequest(`/api/projects/${input.project_id}`);
|
|
210
430
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
211
431
|
});
|
|
212
432
|
|
|
433
|
+
server.tool('update_project', 'Atualiza um projeto', {
|
|
434
|
+
project_id: z.string().describe('ID do projeto'),
|
|
435
|
+
title: z.string().optional(),
|
|
436
|
+
description: z.string().optional(),
|
|
437
|
+
status: z.string().optional().describe('PLANNING, IN_PROGRESS, COMPLETED, ARCHIVED'),
|
|
438
|
+
}, async (input) => {
|
|
439
|
+
const { project_id, ...body } = input;
|
|
440
|
+
const result = await apiRequest(`/api/projects/${project_id}`, { method: 'PUT', body: JSON.stringify(body) });
|
|
441
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
server.tool('delete_project', 'Deleta um projeto e seus modulos', {
|
|
445
|
+
project_id: z.string().describe('ID do projeto'),
|
|
446
|
+
}, async (input) => {
|
|
447
|
+
const result = await apiRequest(`/api/projects/${input.project_id}`, { method: 'DELETE' });
|
|
448
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ── Modules ──
|
|
452
|
+
|
|
453
|
+
server.tool('add_module', 'Adiciona modulo a um projeto', {
|
|
454
|
+
project_id: z.string().describe('ID do projeto'),
|
|
455
|
+
title: z.string().describe('Titulo do modulo'),
|
|
456
|
+
content: z.string().optional().describe('Conteudo/descricao'),
|
|
457
|
+
order: z.number().optional().describe('Posicao na lista'),
|
|
458
|
+
}, async (input) => {
|
|
459
|
+
const { project_id, ...body } = input;
|
|
460
|
+
const result = await apiRequest(`/api/projects/${project_id}/modules`, {
|
|
461
|
+
method: 'POST', body: JSON.stringify(body),
|
|
462
|
+
});
|
|
463
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
server.tool('update_module', 'Atualiza um modulo', {
|
|
467
|
+
project_id: z.string().describe('ID do projeto'),
|
|
468
|
+
module_id: z.string().describe('ID do modulo'),
|
|
469
|
+
title: z.string().optional(),
|
|
470
|
+
content: z.string().optional(),
|
|
471
|
+
isRecorded: z.boolean().optional().describe('Marcar como gravado'),
|
|
472
|
+
driveLink: z.string().optional().describe('Link do Google Drive'),
|
|
473
|
+
}, async (input) => {
|
|
474
|
+
const { project_id, module_id, ...body } = input;
|
|
475
|
+
const result = await apiRequest(`/api/projects/${project_id}/modules/${module_id}`, {
|
|
476
|
+
method: 'PUT', body: JSON.stringify(body),
|
|
477
|
+
});
|
|
478
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
server.tool('delete_module', 'Remove modulo de um projeto', {
|
|
482
|
+
project_id: z.string().describe('ID do projeto'),
|
|
483
|
+
module_id: z.string().describe('ID do modulo'),
|
|
484
|
+
}, async (input) => {
|
|
485
|
+
const result = await apiRequest(`/api/projects/${input.project_id}/modules/${input.module_id}`, {
|
|
486
|
+
method: 'DELETE',
|
|
487
|
+
});
|
|
488
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ── Video Clips ──
|
|
492
|
+
|
|
493
|
+
server.tool('analyze_youtube_video', 'Analisa video do YouTube: baixa, transcreve e encontra melhores momentos', {
|
|
494
|
+
url: z.string().describe('URL do video do YouTube'),
|
|
495
|
+
whisper_model: z.string().optional().describe('Modelo Whisper: tiny, base, small, medium, large'),
|
|
496
|
+
max_moments: z.number().optional().describe('Maximo de momentos (default: 10)'),
|
|
497
|
+
language: z.string().optional().describe('Idioma: pt, en, es'),
|
|
498
|
+
}, async (input) => {
|
|
499
|
+
const result = await apiRequest('/api/video-clips/analyze', {
|
|
500
|
+
method: 'POST', body: JSON.stringify(input),
|
|
501
|
+
});
|
|
502
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
server.tool('cut_youtube_clips', 'Corta clips de video ja analisado', {
|
|
506
|
+
video_clip_id: z.string().describe('ID do video clip'),
|
|
507
|
+
clips: z.array(z.object({
|
|
508
|
+
start: z.number().describe('Segundo inicial'),
|
|
509
|
+
end: z.number().describe('Segundo final'),
|
|
510
|
+
title: z.string().optional().describe('Titulo do clip'),
|
|
511
|
+
})).describe('Lista de clips para cortar'),
|
|
512
|
+
format: z.string().optional().describe('Formato: vertical, square, horizontal'),
|
|
513
|
+
burn_subs: z.boolean().optional().describe('Queimar legendas no video'),
|
|
514
|
+
}, async (input) => {
|
|
515
|
+
const result = await apiRequest(`/api/video-clips/${input.video_clip_id}/cut`, {
|
|
516
|
+
method: 'POST', body: JSON.stringify({
|
|
517
|
+
clips: input.clips,
|
|
518
|
+
format: input.format,
|
|
519
|
+
burnSubs: input.burn_subs,
|
|
520
|
+
}),
|
|
521
|
+
});
|
|
522
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
server.tool('list_video_clips', 'Lista video clips com status', {
|
|
526
|
+
status: z.string().optional().describe('PENDING, ANALYZING, ANALYZED, CLIPPING, READY, FAILED'),
|
|
527
|
+
page: z.string().optional().describe('Pagina'),
|
|
528
|
+
limit: z.string().optional().describe('Itens por pagina'),
|
|
529
|
+
}, async (input) => {
|
|
530
|
+
const params = new URLSearchParams();
|
|
531
|
+
if (input.status) params.set('status', input.status);
|
|
532
|
+
if (input.page) params.set('page', input.page);
|
|
533
|
+
if (input.limit) params.set('limit', input.limit);
|
|
534
|
+
const result = await apiRequest(`/api/video-clips?${params}`);
|
|
535
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ── Update Post & Mixed Carousel ──
|
|
539
|
+
|
|
540
|
+
server.tool('update_post', 'Atualiza um post existente (legenda, hashtags, agendamento). Se o post estiver agendado e voce mudar a data, o agendamento e atualizado automaticamente', {
|
|
541
|
+
post_id: z.string().describe('ID do post'),
|
|
542
|
+
caption: z.string().optional().describe('Nova legenda'),
|
|
543
|
+
hashtags: z.array(z.string()).optional().describe('Novas hashtags'),
|
|
544
|
+
scheduled_at: z.string().optional().describe('Nova data/hora de agendamento (ISO 8601). Reagenda automaticamente se ja estiver agendado'),
|
|
545
|
+
status: z.enum(['DRAFT', 'SCHEDULED']).optional().describe('Novo status (DRAFT cancela agendamento)'),
|
|
546
|
+
}, async (input) => {
|
|
547
|
+
const body = {};
|
|
548
|
+
if (input.caption !== undefined) body.caption = input.caption;
|
|
549
|
+
if (input.hashtags !== undefined) body.hashtags = input.hashtags;
|
|
550
|
+
if (input.scheduled_at !== undefined) body.scheduledAt = input.scheduled_at;
|
|
551
|
+
if (input.status !== undefined) body.status = input.status;
|
|
552
|
+
const result = await apiRequest(`/api/posts/${input.post_id}`, {
|
|
553
|
+
method: 'PUT', body: JSON.stringify(body),
|
|
554
|
+
});
|
|
555
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
server.tool(
|
|
559
|
+
'create_mixed_carousel',
|
|
560
|
+
'Cria carrossel misto: capa gerada por IA (Gemini) + slides informativos em HTML/Template. Aceita brand_id para aplicar logo, cores e tom de voz da marca automaticamente',
|
|
561
|
+
{
|
|
562
|
+
cover_prompt: z.string().describe('Prompt para gerar a imagem de capa via IA (primeiro slide)'),
|
|
563
|
+
slides: z.array(z.object({
|
|
564
|
+
title: z.string().describe('Texto principal do slide'),
|
|
565
|
+
subtitle: z.string().optional().describe('Subtitulo do slide'),
|
|
566
|
+
template: z.string().optional().describe('Template: bold-gradient, minimal-dark, neon-card, quote-elegant, stats-impact, split-color'),
|
|
567
|
+
})).min(1).max(9).describe('Lista de slides template (1-9, a capa IA conta como slide 1)'),
|
|
568
|
+
caption: z.string().optional().describe('Legenda do post (gerada automaticamente se nao informada)'),
|
|
569
|
+
hashtags: z.array(z.string()).optional().describe('Lista de hashtags'),
|
|
570
|
+
aspect_ratio: z.enum(['1:1', '4:5', '9:16']).optional().describe('Proporcao: 1:1 (Feed), 4:5 (Retrato), 9:16 (Stories)'),
|
|
571
|
+
tone: z.string().optional().describe('Tom da legenda: educativo, inspirador, humor, noticia'),
|
|
572
|
+
scheduled_at: z.string().optional().describe('Data/hora para agendar (ISO 8601)'),
|
|
573
|
+
brand_id: z.string().optional().describe('ID do brand para aplicar identidade visual (logo + cores + tom + hashtags). Use list_brands antes'),
|
|
574
|
+
apply_brand: z.boolean().optional().describe('Se true (padrao), aplica brand. Se false, ignora mesmo com brand_id'),
|
|
575
|
+
},
|
|
576
|
+
async (input) => {
|
|
577
|
+
const aspectRatio = input.aspect_ratio || '1:1';
|
|
578
|
+
const images = [];
|
|
579
|
+
|
|
580
|
+
// Resolve brand if provided
|
|
581
|
+
let brand = null;
|
|
582
|
+
if (input.brand_id && input.apply_brand !== false) {
|
|
583
|
+
try {
|
|
584
|
+
brand = await apiRequest(`/api/brands/${input.brand_id}`);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
// ignore brand load errors
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Step 1: AI cover (with brand hints in prompt)
|
|
591
|
+
let coverPrompt = input.cover_prompt;
|
|
592
|
+
if (brand) {
|
|
593
|
+
const hints = [];
|
|
594
|
+
if (brand.primaryColor) hints.push(`paleta ${brand.primaryColor} e ${brand.secondaryColor || ''}`);
|
|
595
|
+
if (brand.voiceTone) hints.push(`estilo ${brand.voiceTone}`);
|
|
596
|
+
if (hints.length > 0) coverPrompt = `${input.cover_prompt}. Identidade visual: ${hints.join(', ')}`;
|
|
597
|
+
}
|
|
598
|
+
const cover = await apiRequest('/api/generate/image', {
|
|
599
|
+
method: 'POST', body: JSON.stringify({ prompt: coverPrompt, aspectRatio }),
|
|
600
|
+
});
|
|
601
|
+
images.push({ imageUrl: cover.imageUrl, order: 0, prompt: coverPrompt });
|
|
602
|
+
|
|
603
|
+
// Step 2: Template slides (with brand colors/logo if available)
|
|
604
|
+
const slideResults = await Promise.allSettled(
|
|
605
|
+
input.slides.map((slide) => {
|
|
606
|
+
const body = {
|
|
607
|
+
title: slide.title,
|
|
608
|
+
subtitle: slide.subtitle,
|
|
609
|
+
template: slide.template || 'bold-gradient',
|
|
610
|
+
aspectRatio,
|
|
611
|
+
};
|
|
612
|
+
if (input.brand_id) {
|
|
613
|
+
body.brandId = input.brand_id;
|
|
614
|
+
body.applyBrand = input.apply_brand !== false;
|
|
615
|
+
}
|
|
616
|
+
return apiRequest('/api/generate/template', {
|
|
617
|
+
method: 'POST', body: JSON.stringify(body),
|
|
618
|
+
});
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
for (let i = 0; i < slideResults.length; i++) {
|
|
623
|
+
const r = slideResults[i];
|
|
624
|
+
if (r.status === 'fulfilled') {
|
|
625
|
+
images.push({ imageUrl: r.value.imageUrl, order: images.length, prompt: input.slides[i].title });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (images.length < 2) {
|
|
630
|
+
throw new Error(`Carrossel precisa de pelo menos 2 imagens. Apenas ${images.length} gerada(s) com sucesso.`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Step 3: Caption
|
|
634
|
+
let caption = input.caption;
|
|
635
|
+
let hashtags = input.hashtags;
|
|
636
|
+
if (!caption) {
|
|
637
|
+
const tone = input.tone || (brand && brand.voiceTone) || undefined;
|
|
638
|
+
const result = await apiRequest('/api/generate/caption', {
|
|
639
|
+
method: 'POST', body: JSON.stringify({ topic: input.cover_prompt, tone }),
|
|
640
|
+
});
|
|
641
|
+
caption = result.caption;
|
|
642
|
+
hashtags = hashtags || result.hashtags;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Merge brand default hashtags
|
|
646
|
+
if (brand && brand.defaultHashtags && brand.defaultHashtags.length) {
|
|
647
|
+
const existing = new Set((hashtags || []).map((h) => h.toLowerCase()));
|
|
648
|
+
const merged = [...(hashtags || [])];
|
|
649
|
+
for (const tag of brand.defaultHashtags) {
|
|
650
|
+
if (!existing.has(tag.toLowerCase())) merged.push(tag);
|
|
651
|
+
}
|
|
652
|
+
hashtags = merged;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Step 4: Create post
|
|
656
|
+
const post = await apiRequest('/api/posts', {
|
|
657
|
+
method: 'POST',
|
|
658
|
+
body: JSON.stringify({
|
|
659
|
+
caption, hashtags, source: 'MCP', aspectRatio,
|
|
660
|
+
isCarousel: true, images,
|
|
661
|
+
...(input.scheduled_at ? { scheduledAt: input.scheduled_at } : {}),
|
|
662
|
+
}),
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
content: [{
|
|
667
|
+
type: 'text',
|
|
668
|
+
text: JSON.stringify({
|
|
669
|
+
post_id: post.id,
|
|
670
|
+
caption: post.caption,
|
|
671
|
+
is_carousel: true,
|
|
672
|
+
cover_image: images[0].imageUrl,
|
|
673
|
+
template_slides: images.length - 1,
|
|
674
|
+
total_images: images.length,
|
|
675
|
+
brand_applied: brand ? { id: brand.id, name: brand.name } : null,
|
|
676
|
+
status: post.status,
|
|
677
|
+
scheduled_at: post.scheduledAt || null,
|
|
678
|
+
}, null, 2),
|
|
679
|
+
}],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// ── Brands ──
|
|
685
|
+
|
|
686
|
+
server.tool('list_brands', 'Lista todos os brands cadastrados (identidade visual: logo, cores, produtos, tom de voz). Use ANTES de criar qualquer post visual para perguntar ao usuario qual brand aplicar', {}, async () => {
|
|
687
|
+
const result = await apiRequest('/api/brands');
|
|
688
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
server.tool('get_brand', 'Retorna detalhes completos de um brand especifico', {
|
|
692
|
+
brand_id: z.string().describe('ID do brand'),
|
|
693
|
+
}, async (input) => {
|
|
694
|
+
const result = await apiRequest(`/api/brands/${input.brand_id}`);
|
|
695
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
server.tool('get_default_brand', 'Retorna o brand padrao do usuario (se houver). Util para aplicar automaticamente quando o usuario nao especifica', {}, async () => {
|
|
699
|
+
const result = await apiRequest('/api/brands/default');
|
|
700
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
server.tool('create_brand', 'Cria um novo brand com identidade visual', {
|
|
704
|
+
name: z.string().describe('Nome do brand'),
|
|
705
|
+
logo_url: z.string().optional().describe('URL do logo'),
|
|
706
|
+
primary_color: z.string().optional().describe('Cor primaria em hex (#RRGGBB)'),
|
|
707
|
+
secondary_color: z.string().optional().describe('Cor secundaria em hex (#RRGGBB)'),
|
|
708
|
+
accent_color: z.string().optional().describe('Cor de destaque em hex'),
|
|
709
|
+
font_family: z.string().optional().describe('Familia de fonte preferida'),
|
|
710
|
+
description: z.string().optional().describe('Descricao do brand'),
|
|
711
|
+
voice_tone: z.string().optional().describe('Tom de voz: profissional, descontraido, educativo'),
|
|
712
|
+
products: z.array(z.string()).optional().describe('Lista de produtos/servicos'),
|
|
713
|
+
default_hashtags: z.array(z.string()).optional().describe('Hashtags padrao a aplicar nos posts'),
|
|
714
|
+
is_default: z.boolean().optional().describe('Se este sera o brand padrao'),
|
|
715
|
+
}, async (input) => {
|
|
716
|
+
const body = {
|
|
717
|
+
name: input.name,
|
|
718
|
+
logoUrl: input.logo_url,
|
|
719
|
+
primaryColor: input.primary_color,
|
|
720
|
+
secondaryColor: input.secondary_color,
|
|
721
|
+
accentColor: input.accent_color,
|
|
722
|
+
fontFamily: input.font_family,
|
|
723
|
+
description: input.description,
|
|
724
|
+
voiceTone: input.voice_tone,
|
|
725
|
+
products: input.products,
|
|
726
|
+
defaultHashtags: input.default_hashtags,
|
|
727
|
+
isDefault: input.is_default,
|
|
728
|
+
};
|
|
729
|
+
const result = await apiRequest('/api/brands', {
|
|
730
|
+
method: 'POST', body: JSON.stringify(body),
|
|
731
|
+
});
|
|
732
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
server.tool('update_brand', 'Atualiza um brand existente', {
|
|
736
|
+
brand_id: z.string().describe('ID do brand'),
|
|
737
|
+
name: z.string().optional(),
|
|
738
|
+
logo_url: z.string().optional(),
|
|
739
|
+
primary_color: z.string().optional(),
|
|
740
|
+
secondary_color: z.string().optional(),
|
|
741
|
+
accent_color: z.string().optional(),
|
|
742
|
+
font_family: z.string().optional(),
|
|
743
|
+
description: z.string().optional(),
|
|
744
|
+
voice_tone: z.string().optional(),
|
|
745
|
+
products: z.array(z.string()).optional(),
|
|
746
|
+
default_hashtags: z.array(z.string()).optional(),
|
|
747
|
+
is_default: z.boolean().optional(),
|
|
748
|
+
}, async (input) => {
|
|
749
|
+
const body = {};
|
|
750
|
+
if (input.name !== undefined) body.name = input.name;
|
|
751
|
+
if (input.logo_url !== undefined) body.logoUrl = input.logo_url;
|
|
752
|
+
if (input.primary_color !== undefined) body.primaryColor = input.primary_color;
|
|
753
|
+
if (input.secondary_color !== undefined) body.secondaryColor = input.secondary_color;
|
|
754
|
+
if (input.accent_color !== undefined) body.accentColor = input.accent_color;
|
|
755
|
+
if (input.font_family !== undefined) body.fontFamily = input.font_family;
|
|
756
|
+
if (input.description !== undefined) body.description = input.description;
|
|
757
|
+
if (input.voice_tone !== undefined) body.voiceTone = input.voice_tone;
|
|
758
|
+
if (input.products !== undefined) body.products = input.products;
|
|
759
|
+
if (input.default_hashtags !== undefined) body.defaultHashtags = input.default_hashtags;
|
|
760
|
+
if (input.is_default !== undefined) body.isDefault = input.is_default;
|
|
761
|
+
const result = await apiRequest(`/api/brands/${input.brand_id}`, {
|
|
762
|
+
method: 'PUT', body: JSON.stringify(body),
|
|
763
|
+
});
|
|
764
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
server.tool('set_default_brand', 'Define um brand como padrao (desmarca os outros automaticamente)', {
|
|
768
|
+
brand_id: z.string().describe('ID do brand a tornar padrao'),
|
|
769
|
+
}, async (input) => {
|
|
770
|
+
const result = await apiRequest(`/api/brands/${input.brand_id}/default`, { method: 'PUT' });
|
|
771
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
server.tool('delete_brand', 'Remove um brand', {
|
|
775
|
+
brand_id: z.string().describe('ID do brand a remover'),
|
|
776
|
+
}, async (input) => {
|
|
777
|
+
const result = await apiRequest(`/api/brands/${input.brand_id}`, { method: 'DELETE' });
|
|
778
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
779
|
+
});
|
|
780
|
+
|
|
213
781
|
// ── Start ──
|
|
214
782
|
|
|
215
783
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openhive-mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "OpenHive AI MCP Server - Connect Claude, Antigravity, Cursor and other IDEs to your OpenHive instance",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "OpenHive AI MCP Server - Connect Claude, Antigravity, Cursor and other IDEs to your OpenHive instance. 35 tools including Brands and Mixed Carousel.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"openhive-mcp": "./index.js"
|