openhive-mcp-server 1.0.0 → 1.1.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 +357 -32
- package/package.json +1 -1
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.1.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,141 @@ 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
|
+
|
|
213
538
|
// ── Start ──
|
|
214
539
|
|
|
215
540
|
async function main() {
|