elo-skills-mcp 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from './src/server.js'
3
+
4
+ startServer().catch(err => {
5
+ console.error('Erro ao iniciar servidor ELO Skills MCP:', err)
6
+ process.exit(1)
7
+ })
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "elo-skills-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Método ELO Criativo — Skills de Estratégia de Conteúdo para Claude",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "elo-skills-mcp": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "dev": "node --watch index.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.10.2"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "claude",
20
+ "conteudo",
21
+ "estrategia",
22
+ "elo"
23
+ ],
24
+ "author": "Ramonnielly Silva Morais",
25
+ "license": "UNLICENSED",
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ }
29
+ }
package/src/brand.js ADDED
@@ -0,0 +1,68 @@
1
+ // Leitura e escrita da config de marca da usuária
2
+ // Salva em ~/.elo/[slug].json
3
+
4
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs'
5
+ import { homedir } from 'os'
6
+ import { join } from 'path'
7
+
8
+ const ELO_DIR = join(homedir(), '.elo')
9
+
10
+ function ensureDir() {
11
+ if (!existsSync(ELO_DIR)) mkdirSync(ELO_DIR, { recursive: true })
12
+ }
13
+
14
+ export function getBrandConfig(slug = 'minha-marca') {
15
+ ensureDir()
16
+ const path = join(ELO_DIR, `${slug}.json`)
17
+ if (!existsSync(path)) return null
18
+ try {
19
+ return JSON.parse(readFileSync(path, 'utf-8'))
20
+ } catch {
21
+ return null
22
+ }
23
+ }
24
+
25
+ export function saveBrandConfig(slug = 'minha-marca', config) {
26
+ ensureDir()
27
+ const path = join(ELO_DIR, `${slug}.json`)
28
+ writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8')
29
+ return path
30
+ }
31
+
32
+ export function listBrands() {
33
+ ensureDir()
34
+ return readdirSync(ELO_DIR)
35
+ .filter(f => f.endsWith('.json'))
36
+ .map(f => f.replace('.json', ''))
37
+ }
38
+
39
+ export function formatBrandContext(brand) {
40
+ if (!brand) return ''
41
+
42
+ return `
43
+ ## IDENTIDADE DE MARCA
44
+
45
+ **Nome/Marca:** ${brand.nome} — ${brand.marca}
46
+ **Nicho:** ${brand.nicho}
47
+ **Posicionamento:** ${brand.posicionamento}
48
+
49
+ **Avatar (cliente ideal):**
50
+ ${brand.avatar}
51
+
52
+ **Anti-avatar (quem NÃO é seu cliente):**
53
+ ${brand.anti_avatar || 'Não definido'}
54
+
55
+ **Tom de voz:** ${brand.tom_voz}
56
+
57
+ **Palavras que USA:** ${(brand.palavras_usar || []).join(', ')}
58
+ **Palavras que EVITA:** ${(brand.palavras_evitar || []).join(', ')}
59
+
60
+ **Bordões e expressões-marca:**
61
+ ${(brand.bordoes || []).map(b => `- "${b}"`).join('\n')}
62
+
63
+ **Arquétipos:** ${(brand.arquetipos || []).join(', ')}
64
+
65
+ **Produto principal:** ${brand.produto || 'Não definido'}
66
+ **Objetivo de negócio:** ${brand.objetivo || 'Não definido'}
67
+ `.trim()
68
+ }
package/src/license.js ADDED
@@ -0,0 +1,66 @@
1
+ // Validação de licença via Supabase
2
+ // Tabela: licenses (key TEXT, active BOOLEAN, expires_at TIMESTAMPTZ, owner_name TEXT)
3
+
4
+ const SUPABASE_URL = 'https://dzicopswqijljhrimtee.supabase.co'
5
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImR6aWNvcHN3cWlqbGpocmltdGVlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU2NzExNjUsImV4cCI6MjA5MTI0NzE2NX0.JgL9jhJIxqQLm8aeKNLKh8HOS2idGqUOslwAqJUMoaY'
6
+
7
+ let cache = null
8
+ let cacheTime = 0
9
+ const CACHE_TTL = 1000 * 60 * 60 // 1 hora
10
+
11
+ export async function validateLicense() {
12
+ const key = process.env.ELO_LICENSE_KEY
13
+
14
+ if (!key) {
15
+ throw new Error(
16
+ '❌ Licença não encontrada.\n\n' +
17
+ 'Adicione sua chave de licença no arquivo de configuração do Claude Code:\n\n' +
18
+ '"ELO_LICENSE_KEY": "SUA-CHAVE-AQUI"\n\n' +
19
+ 'Adquira sua licença em: ramonniellymorais.com.br/elo-skills'
20
+ )
21
+ }
22
+
23
+ // Cache válido — não bate no Supabase a cada chamada
24
+ const now = Date.now()
25
+ if (cache && now - cacheTime < CACHE_TTL) return cache
26
+
27
+ try {
28
+ const res = await fetch(
29
+ `${SUPABASE_URL}/rest/v1/licenses?key=eq.${encodeURIComponent(key)}&select=active,expires_at,owner_name`,
30
+ {
31
+ headers: {
32
+ 'apikey': SUPABASE_ANON_KEY,
33
+ 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
34
+ }
35
+ }
36
+ )
37
+
38
+ if (!res.ok) throw new Error('Erro ao verificar licença.')
39
+
40
+ const data = await res.json()
41
+
42
+ if (!data.length) {
43
+ throw new Error('❌ Chave de licença inválida. Verifique a chave ou adquira em ramonniellymorais.com.br/elo-skills')
44
+ }
45
+
46
+ const license = data[0]
47
+
48
+ if (!license.active) {
49
+ throw new Error('❌ Sua licença está desativada. Entre em contato: contato@ramonniellymorais.com.br')
50
+ }
51
+
52
+ if (license.expires_at && new Date(license.expires_at) < new Date()) {
53
+ throw new Error('❌ Sua licença expirou. Renove em ramonniellymorais.com.br/elo-skills')
54
+ }
55
+
56
+ cache = license
57
+ cacheTime = now
58
+ return license
59
+
60
+ } catch (err) {
61
+ if (err.message.startsWith('❌')) throw err
62
+ // Fallback offline: se Supabase estiver fora, usa cache anterior
63
+ if (cache) return cache
64
+ throw new Error('❌ Não foi possível verificar a licença. Verifique sua conexão.')
65
+ }
66
+ }
package/src/server.js ADDED
@@ -0,0 +1,69 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
4
+ import { validateLicense } from './license.js'
5
+ import * as tools from './tools/index.js'
6
+
7
+ // Lista de todas as ferramentas registradas
8
+ const ALL_TOOLS = Object.values(tools)
9
+
10
+ export async function createServer() {
11
+ const server = new Server(
12
+ { name: 'elo-skills-mcp', version: '1.0.0' },
13
+ { capabilities: { tools: {} } }
14
+ )
15
+
16
+ // Lista as ferramentas disponíveis
17
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
18
+ tools: ALL_TOOLS.map(tool => ({
19
+ name: tool.name,
20
+ description: tool.description,
21
+ inputSchema: tool.inputSchema
22
+ }))
23
+ }))
24
+
25
+ // Executa uma ferramenta
26
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
27
+ const { name, arguments: args } = request.params
28
+
29
+ // Valida licença antes de qualquer execução
30
+ // Setup é liberado sem licença para facilitar o onboarding
31
+ const FREE_TOOLS = ['elo_setup']
32
+ if (!FREE_TOOLS.includes(name)) {
33
+ try {
34
+ await validateLicense()
35
+ } catch (err) {
36
+ return {
37
+ content: [{ type: 'text', text: err.message }],
38
+ isError: true
39
+ }
40
+ }
41
+ }
42
+
43
+ // Encontra e executa a ferramenta
44
+ const tool = ALL_TOOLS.find(t => t.name === name)
45
+ if (!tool) {
46
+ return {
47
+ content: [{ type: 'text', text: `Ferramenta "${name}" não encontrada.` }],
48
+ isError: true
49
+ }
50
+ }
51
+
52
+ try {
53
+ return await tool.handler(args || {})
54
+ } catch (err) {
55
+ return {
56
+ content: [{ type: 'text', text: `Erro ao executar ${name}: ${err.message}` }],
57
+ isError: true
58
+ }
59
+ }
60
+ })
61
+
62
+ return server
63
+ }
64
+
65
+ export async function startServer() {
66
+ const server = await createServer()
67
+ const transport = new StdioServerTransport()
68
+ await server.connect(transport)
69
+ }
@@ -0,0 +1,319 @@
1
+ // Skills de criação de conteúdo: post, reels, stories, legenda, hooks
2
+
3
+ import { getBrandConfig, formatBrandContext } from '../brand.js'
4
+
5
+ function brandParam() {
6
+ return {
7
+ marca: { type: 'string', description: 'Slug da marca configurada (ex: minha-marca). Se omitido, usa a primeira marca salva.' }
8
+ }
9
+ }
10
+
11
+ function resolveContext(marca) {
12
+ const config = getBrandConfig(marca || 'minha-marca')
13
+ if (!config) {
14
+ return `⚠️ Marca "${marca || 'minha-marca'}" não configurada. Use elo_setup primeiro.`
15
+ }
16
+ return formatBrandContext(config)
17
+ }
18
+
19
+ // ─── POST ────────────────────────────────────────────────────────────────────
20
+
21
+ export const postTool = {
22
+ name: 'elo_post',
23
+ description: 'Cria um post completo para Instagram com estrutura ELO. Inclui: gancho, desenvolvimento, CTA, sugestão de formato, etapa C.R.I.A. e KPI.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ tema: { type: 'string', description: 'Tema ou assunto do post' },
28
+ formato: { type: 'string', enum: ['carrossel', 'reels', 'foto', 'livre'], description: 'Formato preferido' },
29
+ cria: { type: 'string', enum: ['C', 'R', 'I', 'A'], description: 'Etapa do C.R.I.A. (Conexão/Repertório/Identidade/Ação)' },
30
+ ...brandParam()
31
+ },
32
+ required: ['tema']
33
+ },
34
+ handler: async ({ tema, formato, cria, marca }) => {
35
+ const ctx = resolveContext(marca)
36
+ return {
37
+ content: [{
38
+ type: 'text',
39
+ text: `${ctx}
40
+
41
+ ---
42
+
43
+ # INSTRUÇÃO — POST INSTAGRAM COM MÉTODO ELO
44
+
45
+ Crie um post completo para Instagram sobre: **"${tema}"**
46
+ ${formato ? `Formato preferido: ${formato}` : ''}
47
+ ${cria ? `Etapa do C.R.I.A.: ${cria}` : ''}
48
+
49
+ ## ESTRUTURA OBRIGATÓRIA
50
+
51
+ ### 1. DIAGNÓSTICO ESTRATÉGICO (silencioso — não mostre para a usuária)
52
+ - Qual etapa do C.R.I.A. este post serve?
53
+ - Qual gatilho mental será ativado?
54
+ - Qual nível de consciência do avatar?
55
+
56
+ ### 2. GANCHO (primeira linha — máx 1 frase)
57
+ Use a identidade verbal da marca. Provoque, gere identificação ou quebre expectativa.
58
+ NUNCA comece com "Você sabia que..." ou "Hoje vou falar sobre..."
59
+
60
+ ### 3. DESENVOLVIMENTO (3 a 5 parágrafos)
61
+ - Alterne frases longas com frases curtíssimas de impacto
62
+ - Inclua pelo menos 1 número, dado ou exemplo real
63
+ - Use o vocabulário da marca
64
+
65
+ ### 4. VIRADA
66
+ Momento de reframe ou insight inesperado. A frase mais forte do post.
67
+
68
+ ### 5. CTA (convite, não ordem)
69
+ Alinhado com o objetivo de negócio. Nunca forçado.
70
+
71
+ ### 6. LEGENDA TÉCNICA
72
+ - Sugestão de formato visual
73
+ - 3 a 5 hashtags estratégicas (não genéricas)
74
+ - Melhor horário de publicação
75
+
76
+ ### 7. MÉTRICAS
77
+ - Etapa C.R.I.A.: [C/R/I/A]
78
+ - KPI principal: [alcance / salvamentos / cliques / conversões]
79
+ - Gatilho ativado: [nome do gatilho]`
80
+ }]
81
+ }
82
+ }
83
+ }
84
+
85
+ // ─── REELS ───────────────────────────────────────────────────────────────────
86
+
87
+ export const reelsTool = {
88
+ name: 'elo_reels',
89
+ description: 'Cria roteiro completo de Reels (30 segundos) com cortes, legenda e análise estratégica.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ tema: { type: 'string', description: 'Tema do Reels' },
94
+ estilo: { type: 'string', enum: ['dialogo_interno', 'quebra_padrao', 'broll_narracao', 'livre'], description: 'Estilo visual' },
95
+ ...brandParam()
96
+ },
97
+ required: ['tema']
98
+ },
99
+ handler: async ({ tema, estilo, marca }) => {
100
+ const ctx = resolveContext(marca)
101
+ return {
102
+ content: [{
103
+ type: 'text',
104
+ text: `${ctx}
105
+
106
+ ---
107
+
108
+ # INSTRUÇÃO — ROTEIRO DE REELS 30s
109
+
110
+ Tema: **"${tema}"**
111
+ ${estilo ? `Estilo: ${estilo}` : ''}
112
+
113
+ ## ESTRUTURA DO ROTEIRO
114
+
115
+ ### GANCHO (0–3s)
116
+ A primeira frase dita ou o texto na tela. Deve parar o scroll. Use a voz da marca.
117
+
118
+ ### CORTE 1 (3–8s)
119
+ Desenvolvimento inicial. Problema ou provocação.
120
+
121
+ ### CORTE 2 (8–16s)
122
+ Tensão ou dado surpreendente.
123
+
124
+ ### CORTE 3 (16–24s)
125
+ Virada / insight principal.
126
+
127
+ ### FECHAMENTO (24–30s)
128
+ CTA falado + texto na tela. Simples, direto.
129
+
130
+ ---
131
+
132
+ ## LEGENDA DO REELS
133
+ (máx 150 caracteres — continua nos comentários se necessário)
134
+
135
+ ## TEXTO NA TELA (overlays)
136
+ Liste cada texto que aparece na tela, em ordem.
137
+
138
+ ## ANÁLISE ESTRATÉGICA
139
+ - Avatar ativado
140
+ - Etapa C.R.I.A.
141
+ - Gatilho principal
142
+ - KPI esperado: [alcance / compartilhamentos / salvamentos]`
143
+ }]
144
+ }
145
+ }
146
+ }
147
+
148
+ // ─── STORIES ─────────────────────────────────────────────────────────────────
149
+
150
+ export const storiesTool = {
151
+ name: 'elo_stories',
152
+ description: 'Cria sequência de 5 stories com 5 ângulos diferentes, análise neurológica e score de impacto.',
153
+ inputSchema: {
154
+ type: 'object',
155
+ properties: {
156
+ tema: { type: 'string', description: 'Tema ou objetivo da sequência' },
157
+ objetivo: { type: 'string', enum: ['engajamento', 'autoridade', 'venda', 'bastidor', 'livre'], description: 'Objetivo da sequência' },
158
+ ...brandParam()
159
+ },
160
+ required: ['tema']
161
+ },
162
+ handler: async ({ tema, objetivo, marca }) => {
163
+ const ctx = resolveContext(marca)
164
+ return {
165
+ content: [{
166
+ type: 'text',
167
+ text: `${ctx}
168
+
169
+ ---
170
+
171
+ # INSTRUÇÃO — SEQUÊNCIA DE 5 STORIES
172
+
173
+ Tema: **"${tema}"**
174
+ ${objetivo ? `Objetivo: ${objetivo}` : ''}
175
+
176
+ ## PARA CADA STORY (1 a 5):
177
+
178
+ **Story [N] — [Nome do ângulo]**
179
+ - **Tipo:** (texto / foto / vídeo / enquete / caixinha)
180
+ - **Ângulo:** (provocação / humor / bastidor / dado / virada)
181
+ - **Texto principal:** (exatamente o que vai escrito)
182
+ - **Elemento interativo:** (enquete, caixinha, contagem, link — se aplicável)
183
+ - **Transição:** como leva para o próximo story
184
+
185
+ ---
186
+
187
+ ## OS 5 ÂNGULOS OBRIGATÓRIOS
188
+ 1. Provocação / identificação imediata
189
+ 2. Dado ou prova inesperada
190
+ 3. Bastidor ou história real
191
+ 4. Humor ou ironia com sentido
192
+ 5. Virada + CTA
193
+
194
+ ---
195
+
196
+ ## ANÁLISE NEUROLÓGICA
197
+ Para cada story: qual mecanismo psicológico ativa? (curiosidade, identidade, contraste, pertencimento, urgência)
198
+
199
+ ## SCORE DE IMPACTO
200
+ Dê uma nota de 0 a 10 para a sequência em: Retenção / Engajamento / Conversão
201
+ Justifique cada nota em uma linha.`
202
+ }]
203
+ }
204
+ }
205
+ }
206
+
207
+ // ─── LEGENDA ─────────────────────────────────────────────────────────────────
208
+
209
+ export const legendaTool = {
210
+ name: 'elo_legenda',
211
+ description: 'Cria ou aprimora a legenda de um post pronto. Adiciona: linha de impacto, expansão, momento aha e CTA com sentido.',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ conteudo: { type: 'string', description: 'O post ou conteúdo que precisa de legenda' },
216
+ objetivo: { type: 'string', description: 'Objetivo da legenda: engajar, vender, salvar, compartilhar' },
217
+ ...brandParam()
218
+ },
219
+ required: ['conteudo']
220
+ },
221
+ handler: async ({ conteudo, objetivo, marca }) => {
222
+ const ctx = resolveContext(marca)
223
+ return {
224
+ content: [{
225
+ type: 'text',
226
+ text: `${ctx}
227
+
228
+ ---
229
+
230
+ # INSTRUÇÃO — LEGENDA COM IMPACTO
231
+
232
+ Conteúdo recebido:
233
+ """
234
+ ${conteudo}
235
+ """
236
+
237
+ ${objetivo ? `Objetivo: ${objetivo}` : ''}
238
+
239
+ ## ESTRUTURA DA LEGENDA
240
+
241
+ ### 1. LINHA DE IMPACTO (primeira linha — máx 1 frase)
242
+ A frase que aparece antes do "ver mais". Deve parar o scroll. Use a voz da marca.
243
+
244
+ ### 2. EXPANSÃO (2 a 4 parágrafos)
245
+ Aprofunda o tema. Alterna frases longas com curtas. Inclui 1 dado ou exemplo real.
246
+
247
+ ### 3. MOMENTO AHA
248
+ A frase mais forte. O insight que a pessoa vai querer salvar ou copiar.
249
+
250
+ ### 4. CTA COM SENTIDO
251
+ Convite genuíno, não ordem. Alinhado ao objetivo de negócio.
252
+
253
+ ### 5. HASHTAGS
254
+ 5 hashtags: 2 de nicho + 2 de tema + 1 de comunidade. Sem hashtags genéricas (#vida, #amor).`
255
+ }]
256
+ }
257
+ }
258
+ }
259
+
260
+ // ─── HOOKS ───────────────────────────────────────────────────────────────────
261
+
262
+ export const hooksTool = {
263
+ name: 'elo_hooks',
264
+ description: 'Gera 20 hooks para um tema. Você escolhe os melhores e recebe 10 refinados com variações.',
265
+ inputSchema: {
266
+ type: 'object',
267
+ properties: {
268
+ tema: { type: 'string', description: 'Tema para gerar hooks' },
269
+ formato: { type: 'string', enum: ['reels', 'carrossel', 'post', 'stories', 'qualquer'], description: 'Formato de destino' },
270
+ ...brandParam()
271
+ },
272
+ required: ['tema']
273
+ },
274
+ handler: async ({ tema, formato, marca }) => {
275
+ const ctx = resolveContext(marca)
276
+ return {
277
+ content: [{
278
+ type: 'text',
279
+ text: `${ctx}
280
+
281
+ ---
282
+
283
+ # INSTRUÇÃO — GERADOR DE HOOKS VIRAIS
284
+
285
+ Tema: **"${tema}"**
286
+ ${formato ? `Formato: ${formato}` : ''}
287
+
288
+ ## REGRAS DOS HOOKS
289
+ - NUNCA use "descubra" — é fraco e genérico
290
+ - Prefira perguntas desconfortáveis a afirmações seguras
291
+ - Use a voz e os bordões da marca
292
+ - Cada hook deve parar o scroll em 0,3 segundos
293
+ - Varie os padrões: pergunta / afirmação ousada / dado / paradoxo / inversão
294
+
295
+ ## FASE 1 — GERAÇÃO
296
+
297
+ Crie 20 hooks para o tema. Organize em 4 categorias de 5:
298
+
299
+ **Pergunta Desconfortável (5 hooks)**
300
+ **Afirmação Polarizante (5 hooks)**
301
+ **Dado ou Número Inesperado (5 hooks)**
302
+ **Inversão ou Paradoxo (5 hooks)**
303
+
304
+ ---
305
+
306
+ Após apresentar os 20, peça à usuária para escolher os favoritos.
307
+ Com as escolhas, refine os 10 melhores com variações (versão A e B para cada).
308
+
309
+ ## FASE 2 — REFINAMENTO (após escolha da usuária)
310
+
311
+ Para cada hook escolhido:
312
+ - **Versão A:** mais provocativa
313
+ - **Versão B:** mais intrigante
314
+ - **Melhor formato de destino:** reels / carrossel / stories
315
+ - **Gatilho ativado:** curiosidade / identidade / contraste / urgência`
316
+ }]
317
+ }
318
+ }
319
+ }