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.
Files changed (2) hide show
  1. package/index.js +357 -32
  2. 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 || 'API request failed');
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.0.0' });
56
+ const server = new McpServer({ name: 'openhive', version: '1.1.0' });
56
57
 
57
58
  // ── Posts ──
58
59
 
59
- server.tool('create_post', 'Cria um novo post', {
60
- caption: z.string().optional().describe('Legenda do post'),
61
- imageUrl: z.string().optional().describe('URL da imagem'),
62
- hashtags: z.array(z.string()).optional().describe('Lista de hashtags'),
63
- scheduledAt: z.string().optional().describe('Data de agendamento ISO'),
64
- }, async (input) => {
65
- const result = await apiRequest('/api/posts', { method: 'POST', body: JSON.stringify(input) });
66
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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 result = await apiRequest(`/api/posts/${input.post_id}/publish`, { method: 'POST' });
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 com IA (Gemini)', {
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 com IA', {
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('render_html_to_image', 'Renderiza HTML/CSS/Tailwind em imagem PNG', {
120
- html: z.string().describe('Codigo HTML do post (suporta Tailwind CSS)'),
121
- width: z.number().optional().describe('Largura em pixels (default: 1080)'),
122
- height: z.number().optional().describe('Altura em pixels (default: 1080)'),
123
- }, async (input) => {
124
- const result = await apiRequest('/api/generate/html', {
125
- method: 'POST', body: JSON.stringify(input),
126
- });
127
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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
- status: z.string().optional(),
173
- priority: z.string().optional(),
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhive-mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "OpenHive AI MCP Server - Connect Claude, Antigravity, Cursor and other IDEs to your OpenHive instance",
5
5
  "main": "index.js",
6
6
  "bin": {