openhive-mcp-server 1.1.0 → 1.3.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 +252 -1
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -53,7 +53,7 @@ async function apiRequest(path, options = {}) {
|
|
|
53
53
|
return data.data;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const server = new McpServer({ name: 'openhive', version: '1.
|
|
56
|
+
const server = new McpServer({ name: 'openhive', version: '1.3.0' });
|
|
57
57
|
|
|
58
58
|
// ── Posts ──
|
|
59
59
|
|
|
@@ -535,6 +535,257 @@ server.tool('list_video_clips', 'Lista video clips com status', {
|
|
|
535
535
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
536
536
|
});
|
|
537
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. Aceita website_url e instagram_url para que agentes possam pesquisar informacoes da marca', {
|
|
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
|
+
website_url: z.string().optional().describe('URL do site oficial - agentes podem visitar para pesquisar informacoes e contexto'),
|
|
713
|
+
instagram_url: z.string().optional().describe('URL do perfil Instagram - agentes podem analisar o estilo visual e de conteudo para manter consistencia'),
|
|
714
|
+
products: z.array(z.string()).optional().describe('Lista de produtos/servicos'),
|
|
715
|
+
default_hashtags: z.array(z.string()).optional().describe('Hashtags padrao a aplicar nos posts'),
|
|
716
|
+
is_default: z.boolean().optional().describe('Se este sera o brand padrao'),
|
|
717
|
+
}, async (input) => {
|
|
718
|
+
const body = {
|
|
719
|
+
name: input.name,
|
|
720
|
+
logoUrl: input.logo_url,
|
|
721
|
+
primaryColor: input.primary_color,
|
|
722
|
+
secondaryColor: input.secondary_color,
|
|
723
|
+
accentColor: input.accent_color,
|
|
724
|
+
fontFamily: input.font_family,
|
|
725
|
+
description: input.description,
|
|
726
|
+
voiceTone: input.voice_tone,
|
|
727
|
+
websiteUrl: input.website_url,
|
|
728
|
+
instagramUrl: input.instagram_url,
|
|
729
|
+
products: input.products,
|
|
730
|
+
defaultHashtags: input.default_hashtags,
|
|
731
|
+
isDefault: input.is_default,
|
|
732
|
+
};
|
|
733
|
+
const result = await apiRequest('/api/brands', {
|
|
734
|
+
method: 'POST', body: JSON.stringify(body),
|
|
735
|
+
});
|
|
736
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
server.tool('update_brand', 'Atualiza um brand existente', {
|
|
740
|
+
brand_id: z.string().describe('ID do brand'),
|
|
741
|
+
name: z.string().optional(),
|
|
742
|
+
logo_url: z.string().optional(),
|
|
743
|
+
primary_color: z.string().optional(),
|
|
744
|
+
secondary_color: z.string().optional(),
|
|
745
|
+
accent_color: z.string().optional(),
|
|
746
|
+
font_family: z.string().optional(),
|
|
747
|
+
description: z.string().optional(),
|
|
748
|
+
voice_tone: z.string().optional(),
|
|
749
|
+
website_url: z.string().optional(),
|
|
750
|
+
instagram_url: z.string().optional(),
|
|
751
|
+
products: z.array(z.string()).optional(),
|
|
752
|
+
default_hashtags: z.array(z.string()).optional(),
|
|
753
|
+
is_default: z.boolean().optional(),
|
|
754
|
+
}, async (input) => {
|
|
755
|
+
const body = {};
|
|
756
|
+
if (input.name !== undefined) body.name = input.name;
|
|
757
|
+
if (input.logo_url !== undefined) body.logoUrl = input.logo_url;
|
|
758
|
+
if (input.primary_color !== undefined) body.primaryColor = input.primary_color;
|
|
759
|
+
if (input.secondary_color !== undefined) body.secondaryColor = input.secondary_color;
|
|
760
|
+
if (input.accent_color !== undefined) body.accentColor = input.accent_color;
|
|
761
|
+
if (input.font_family !== undefined) body.fontFamily = input.font_family;
|
|
762
|
+
if (input.description !== undefined) body.description = input.description;
|
|
763
|
+
if (input.voice_tone !== undefined) body.voiceTone = input.voice_tone;
|
|
764
|
+
if (input.website_url !== undefined) body.websiteUrl = input.website_url;
|
|
765
|
+
if (input.instagram_url !== undefined) body.instagramUrl = input.instagram_url;
|
|
766
|
+
if (input.products !== undefined) body.products = input.products;
|
|
767
|
+
if (input.default_hashtags !== undefined) body.defaultHashtags = input.default_hashtags;
|
|
768
|
+
if (input.is_default !== undefined) body.isDefault = input.is_default;
|
|
769
|
+
const result = await apiRequest(`/api/brands/${input.brand_id}`, {
|
|
770
|
+
method: 'PUT', body: JSON.stringify(body),
|
|
771
|
+
});
|
|
772
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
server.tool('set_default_brand', 'Define um brand como padrao (desmarca os outros automaticamente)', {
|
|
776
|
+
brand_id: z.string().describe('ID do brand a tornar padrao'),
|
|
777
|
+
}, async (input) => {
|
|
778
|
+
const result = await apiRequest(`/api/brands/${input.brand_id}/default`, { method: 'PUT' });
|
|
779
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
server.tool('delete_brand', 'Remove um brand', {
|
|
783
|
+
brand_id: z.string().describe('ID do brand a remover'),
|
|
784
|
+
}, async (input) => {
|
|
785
|
+
const result = await apiRequest(`/api/brands/${input.brand_id}`, { method: 'DELETE' });
|
|
786
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
787
|
+
});
|
|
788
|
+
|
|
538
789
|
// ── Start ──
|
|
539
790
|
|
|
540
791
|
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.3.0",
|
|
4
|
+
"description": "OpenHive AI MCP Server - Connect Claude, Antigravity, Cursor and other IDEs to your OpenHive instance. 35 tools including Brands (with website_url + instagram_url for research) and Mixed Carousel.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"openhive-mcp": "./index.js"
|