sapiens-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/README.md +45 -0
- package/dist/convexClient.js +167 -0
- package/dist/index.js +137 -0
- package/dist/tools/article.js +155 -0
- package/dist/tools/community.js +74 -0
- package/dist/tools/gallery.js +57 -0
- package/dist/tools/helen.js +120 -0
- package/dist/tools/image.js +145 -0
- package/dist/tools/meta.js +241 -0
- package/dist/tools/musicator.js +77 -0
- package/dist/tools/persona.js +87 -0
- package/dist/tools/pipeline.js +294 -0
- package/dist/tools/quotePop.js +175 -0
- package/dist/tools/repertorio.js +217 -0
- package/dist/tools/search.js +60 -0
- package/dist/tools/shorts.js +79 -0
- package/dist/tools/studios.js +169 -0
- package/dist/tools/video.js +55 -0
- package/dist/tools/write.js +132 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# sapiens-mcp
|
|
2
|
+
|
|
3
|
+
Servidor MCP pra operar o [Sapiens Sintéticos](https://sapiensinteticos.com) direto do Claude Code, **na sua própria conta**. Você pede no Claude ("gera uma imagem disso", "escreve um artigo sobre aquilo") e ele faz, gastando as **suas Sinapses**, salvando no **seu perfil**.
|
|
4
|
+
|
|
5
|
+
## Pré-requisito
|
|
6
|
+
|
|
7
|
+
Uma conta no Sapiens Sintéticos. Conta e login acontecem só no site (Google ou email), nunca por aqui. Não tem conta? Crie em [sapiensinteticos.com](https://sapiensinteticos.com).
|
|
8
|
+
|
|
9
|
+
## Instalar (Claude Code)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
claude mcp add sapiens -- npx -y sapiens-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Precisa de Node 18+. A URL do backend já vem embutida; não precisa configurar nada.
|
|
16
|
+
|
|
17
|
+
## Conectar sua conta
|
|
18
|
+
|
|
19
|
+
1. Abra **[sapiensinteticos.com/conectar-claude](https://sapiensinteticos.com/conectar-claude)** logado e gere o código (`XXXX-XXXX`, vale 5 min).
|
|
20
|
+
2. No Claude Code, peça pra logar (ele chama a ferramenta de login), ou rode direto:
|
|
21
|
+
|
|
22
|
+
`sapiens_meta` com `action: "login"` e `code: "XXXX-XXXX"`
|
|
23
|
+
|
|
24
|
+
O token de 30 dias fica salvo em `~/.sapiens-mcp/session.json`. Pra sair: `sapiens_meta action=logout`.
|
|
25
|
+
|
|
26
|
+
## O que dá pra pedir (e o custo em Sinapses)
|
|
27
|
+
|
|
28
|
+
| O que | Custo |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Gerar imagem (vai pra sua galeria) | ~400-500 |
|
|
31
|
+
| Escrever artigo na voz Sapiens (no seu perfil) | 400 |
|
|
32
|
+
| Voz / narração (Helen TTS) | 500 |
|
|
33
|
+
| Letra de música / render (Musicator) | 300 / 3000 |
|
|
34
|
+
| Arte de persona (MBTI) | 450 |
|
|
35
|
+
| Repertório, listar/editar seus artigos, ver saldo | grátis |
|
|
36
|
+
|
|
37
|
+
O Claude avisa o custo antes de gastar, e geração que falha é estornada. Publicar no blog editorial, Coluna Sapiens e o pipeline são exclusivos do dono da plataforma.
|
|
38
|
+
|
|
39
|
+
## Privacidade
|
|
40
|
+
|
|
41
|
+
O servidor só conversa com o backend público do Sapiens (Convex). Sua identidade vem sempre do token de login, nunca de parâmetros soltos. Cada conta só mexe no que é dela.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
Feito por [BorderLess](https://sapiensinteticos.com). Licença MIT.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a raiz do monorepo a partir do diretório do MCP build (dist).
|
|
9
|
+
* dist/convexClient.js está em tools/mcp-sapiens/dist, então 3 níveis acima
|
|
10
|
+
* dá no root do monorepo.
|
|
11
|
+
*/
|
|
12
|
+
function repoRoot() {
|
|
13
|
+
return path.resolve(__dirname, "..", "..", "..");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Carrega .env.local de vários lugares conhecidos sem sobrescrever vars já
|
|
17
|
+
* setadas (.mcp.json env tem prioridade). Ordem é importante: variáveis do
|
|
18
|
+
* primeiro arquivo encontrado ganham, próximos arquivos só adicionam keys
|
|
19
|
+
* que ainda não existem.
|
|
20
|
+
*/
|
|
21
|
+
function loadLocalEnv() {
|
|
22
|
+
const candidates = [
|
|
23
|
+
// 1. .env.local do MCP (legado, ainda primário)
|
|
24
|
+
path.resolve(__dirname, "..", ".env.local"),
|
|
25
|
+
// 2. .env.local global do monorepo
|
|
26
|
+
path.resolve(repoRoot(), ".env.local"),
|
|
27
|
+
// 3. .env do monorepo
|
|
28
|
+
path.resolve(repoRoot(), ".env"),
|
|
29
|
+
// 4. apps/sapiens .env.local (compartilha NEXT_PUBLIC_CONVEX_URL)
|
|
30
|
+
path.resolve(repoRoot(), "apps", "sapiens", ".env.local"),
|
|
31
|
+
path.resolve(repoRoot(), "apps", "sapiens", ".env"),
|
|
32
|
+
];
|
|
33
|
+
for (const p of candidates) {
|
|
34
|
+
if (fs.existsSync(p)) {
|
|
35
|
+
const content = fs.readFileSync(p, "utf8");
|
|
36
|
+
for (const line of content.split(/\r?\n/)) {
|
|
37
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
38
|
+
if (m && !process.env[m[1]]) {
|
|
39
|
+
let val = m[2];
|
|
40
|
+
if (val.startsWith('"') && val.endsWith('"'))
|
|
41
|
+
val = val.slice(1, -1);
|
|
42
|
+
process.env[m[1]] = val;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
loadLocalEnv();
|
|
49
|
+
/**
|
|
50
|
+
* Lê session token do plugin Claude Code (state local). Esse arquivo é criado
|
|
51
|
+
* pela skill `/sapiens:login` e é fonte alternativa pro token. MCP env.local
|
|
52
|
+
* continua tendo prioridade pra retrocompat com setups antigos.
|
|
53
|
+
*/
|
|
54
|
+
function readPluginSessionToken() {
|
|
55
|
+
const candidates = [
|
|
56
|
+
path.resolve(repoRoot(), ".claude-plugin", ".local", "session.json"),
|
|
57
|
+
path.resolve(repoRoot(), ".claude-plugin", "session.local.json"),
|
|
58
|
+
];
|
|
59
|
+
for (const p of candidates) {
|
|
60
|
+
if (!fs.existsSync(p))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
64
|
+
if (raw?.sessionToken && raw.sessionToken.length > 8) {
|
|
65
|
+
if (raw.expiresAt && Date.now() > raw.expiresAt) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return raw.sessionToken;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// arquivo corrompido, ignora
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// URL do Convex de produção do Sapiens. É valor público (NEXT_PUBLIC_*), então
|
|
78
|
+
// vem embutido como default pra instalação via npm funcionar sem configurar nada.
|
|
79
|
+
// Override por env (CONVEX_URL) continua valendo pra dev/staging.
|
|
80
|
+
const DEFAULT_CONVEX_URL = "https://oceanic-bass-791.convex.cloud";
|
|
81
|
+
let _client = null;
|
|
82
|
+
export function getConvex() {
|
|
83
|
+
if (_client)
|
|
84
|
+
return _client;
|
|
85
|
+
const url = process.env.CONVEX_URL ||
|
|
86
|
+
process.env.NEXT_PUBLIC_CONVEX_URL ||
|
|
87
|
+
DEFAULT_CONVEX_URL;
|
|
88
|
+
_client = new ConvexHttpClient(url);
|
|
89
|
+
return _client;
|
|
90
|
+
}
|
|
91
|
+
// ============================================
|
|
92
|
+
// Sessão salva pelo login do próprio MCP (sapiens_meta action=login).
|
|
93
|
+
// Caminho canônico pra instalação via npm: ~/.sapiens-mcp/session.json.
|
|
94
|
+
// ============================================
|
|
95
|
+
function sessionStorePath() {
|
|
96
|
+
return path.resolve(os.homedir(), ".sapiens-mcp", "session.json");
|
|
97
|
+
}
|
|
98
|
+
export function saveSessionToken(token) {
|
|
99
|
+
const file = sessionStorePath();
|
|
100
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
101
|
+
// Espelha os 30 dias do server (só pra avisar quando perto de expirar; a
|
|
102
|
+
// validade real é sempre checada no Convex).
|
|
103
|
+
const expiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000;
|
|
104
|
+
fs.writeFileSync(file, JSON.stringify({ sessionToken: token, expiresAt }, null, 2), "utf8");
|
|
105
|
+
return { path: file, expiresAt };
|
|
106
|
+
}
|
|
107
|
+
export function clearSessionToken() {
|
|
108
|
+
try {
|
|
109
|
+
const file = sessionStorePath();
|
|
110
|
+
if (fs.existsSync(file)) {
|
|
111
|
+
fs.unlinkSync(file);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// ignora
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
function readStoredSessionToken() {
|
|
121
|
+
try {
|
|
122
|
+
const file = sessionStorePath();
|
|
123
|
+
if (!fs.existsSync(file))
|
|
124
|
+
return null;
|
|
125
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
126
|
+
if (raw?.sessionToken && raw.sessionToken.length > 8) {
|
|
127
|
+
if (raw.expiresAt && Date.now() > raw.expiresAt)
|
|
128
|
+
return null;
|
|
129
|
+
return raw.sessionToken;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// arquivo corrompido, ignora
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
export function getSessionToken() {
|
|
138
|
+
// Prioridade 1: env var explícita (config do MCP ou .env.local)
|
|
139
|
+
const fromEnv = process.env.SAPIENS_DESKTOP_SESSION_TOKEN;
|
|
140
|
+
if (fromEnv && fromEnv !== "PASTE_HERE_AFTER_RUNNING_auth.mjs") {
|
|
141
|
+
return fromEnv;
|
|
142
|
+
}
|
|
143
|
+
// Prioridade 2: store local do login do MCP (caminho npm)
|
|
144
|
+
const fromStore = readStoredSessionToken();
|
|
145
|
+
if (fromStore) {
|
|
146
|
+
return fromStore;
|
|
147
|
+
}
|
|
148
|
+
// Prioridade 3: state do plugin Claude Code (monorepo, /sapiens:login)
|
|
149
|
+
const fromPlugin = readPluginSessionToken();
|
|
150
|
+
if (fromPlugin) {
|
|
151
|
+
return fromPlugin;
|
|
152
|
+
}
|
|
153
|
+
throw new Error("Conta Sapiens não conectada. 1) Abra https://sapiensinteticos.com/conectar-claude " +
|
|
154
|
+
"logado e gere o código. 2) Rode a tool de login: sapiens_meta action=login code=XXXX-XXXX.");
|
|
155
|
+
}
|
|
156
|
+
export async function convexQuery(fnPath, args) {
|
|
157
|
+
const client = getConvex();
|
|
158
|
+
return (await client.query(fnPath, args));
|
|
159
|
+
}
|
|
160
|
+
export async function convexMutation(fnPath, args) {
|
|
161
|
+
const client = getConvex();
|
|
162
|
+
return (await client.mutation(fnPath, args));
|
|
163
|
+
}
|
|
164
|
+
export async function convexAction(fnPath, args) {
|
|
165
|
+
const client = getConvex();
|
|
166
|
+
return (await client.action(fnPath, args));
|
|
167
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
6
|
+
import { pipeline, pipelineSchema } from "./tools/pipeline.js";
|
|
7
|
+
import { image, imageSchema } from "./tools/image.js";
|
|
8
|
+
import { meta, metaSchema } from "./tools/meta.js";
|
|
9
|
+
import { repertorio, repertorioSchema } from "./tools/repertorio.js";
|
|
10
|
+
import { gallery, gallerySchema } from "./tools/gallery.js";
|
|
11
|
+
import { community, communitySchema } from "./tools/community.js";
|
|
12
|
+
import { article, articleSchema } from "./tools/article.js";
|
|
13
|
+
import { quotePop, quotePopSchema } from "./tools/quotePop.js";
|
|
14
|
+
import { search, searchSchema } from "./tools/search.js";
|
|
15
|
+
import { studios, studiosSchema } from "./tools/studios.js";
|
|
16
|
+
import { persona, personaSchema } from "./tools/persona.js";
|
|
17
|
+
import { helen, helenSchema } from "./tools/helen.js";
|
|
18
|
+
import { musicator, musicatorSchema } from "./tools/musicator.js";
|
|
19
|
+
import { shorts, shortsSchema } from "./tools/shorts.js";
|
|
20
|
+
import { video, videoSchema } from "./tools/video.js";
|
|
21
|
+
import { write, writeSchema } from "./tools/write.js";
|
|
22
|
+
const TOOLS = {
|
|
23
|
+
sapiens_pipeline: {
|
|
24
|
+
description: "CRUD do content pipeline Sapiens (sources/productions/publishables). Sub-actions: list_sources, list_articles (use includeDrafts pra incluir drafts; onlyAvailable pra esconder os já virados em source), get_source, get_production, list_versions, add_article_as_source, create_draft_article_and_source (seed), create_production (sourceId+format → productionId draft), update_production (substitui payload, opcionalmente muda status), finalize_production (cria publishable v1, v2... com snapshot), remove_production, remove_source, set_source_done, update_source_notes, restore_version (volta payload duma versão antiga), propose_mega_grafico_plan (granular: só gera plano via Gemini, devolve fullPrompt+spec), run_mega_grafico_full (ONE-SHOT, recomendado: cria production+propõe plano+gera imagem+aplica selo Sapiens+finaliza publishable numa chamada só). Pra mega_grafico, SEMPRE prefira run_mega_grafico_full em vez de sequenciar manualmente — menos drift, idempotente (passa productionId pra reusar). OBRIGATÓRIO perguntar ao user antes se withHelen=true (cartoon Helen interage com tema, ~15-25% do poster) ou false (poster 100% diagramático). Custo ~900-1000 sinapses por geração. Use skipFinalize=true se quiser deixar production em 'ready' pro admin revisar antes de publishable. Payload livre por formato — chame sapiens_meta action=formats pra ver schemas sugeridos.",
|
|
25
|
+
schema: pipelineSchema,
|
|
26
|
+
handler: pipeline,
|
|
27
|
+
},
|
|
28
|
+
sapiens_image: {
|
|
29
|
+
description: "Operações de imagem via Sapiens (Gemini, Azure, Veo). Sub-actions: 'generate' (gera imagem completa imediato — nanoBanana ou gpt-image-2 com prompt+model+aspectRatio+size, suporta mode=edit/variation com sourceImageId), 'request_generation' (v1.7, cria APENAS row pendente em generatedImages + debita créditos — pra modelos sapiens-video-* ANTES de sapiens_shorts/sapiens_video; whitelist, rate limit 3/min), 'compose' (v1.8, combina persona+screen via Gemini pra app-demo Shorts; 25 sinapses, rate limit 10/min). generate=image one-shot, request_generation=criar row video, compose=montar start frame app-demo.",
|
|
30
|
+
schema: imageSchema,
|
|
31
|
+
handler: image,
|
|
32
|
+
},
|
|
33
|
+
sapiens_meta: {
|
|
34
|
+
description: "Utilitários transversais: login (conecta a conta com o código de sapiensinteticos.com/conectar-claude, salva sessão de 30 dias localmente), logout, whoami (tier user/admin + saldo + email), credits (saldo agregado), subscription (v1.2 — plan + status + saldo por bucket subscription/grants/free + warnings low/critical), formats (schemas por formato), health, app_url (URLs canônicas). Use credits/subscription antes de gerar imagem pra avisar se vai estourar.",
|
|
35
|
+
schema: metaSchema,
|
|
36
|
+
handler: meta,
|
|
37
|
+
},
|
|
38
|
+
sapiens_repertorio: {
|
|
39
|
+
description: "Acervo pessoal de filme/série/anime/jogo. Reads: list (acervo, filtros mediaType/status), search (texto em title/genres/tags), get (detalhe), lists (listas curadas), popArticles (publicados na coluna). Mutations (qualquer logado, mexem no PRÓPRIO acervo): add_item (adiciona ao acervo do user da sessão; source='manual' pra entry hand-rolled), update_item (status/rating/tags/note/isPublic), remove_item.",
|
|
40
|
+
schema: repertorioSchema,
|
|
41
|
+
handler: repertorio,
|
|
42
|
+
},
|
|
43
|
+
sapiens_gallery: {
|
|
44
|
+
description: "Browse das imagens geradas pelo user (nanoBanana). Sub-actions: list (últimas N imagens, com prompt/model/url), get (1 imagem com metadados, opcionalmente base64). Use pra reusar imagem como referência (passe o imageId em sapiens_image mode=edit ou mode=variation), ou pra mostrar pro user o que ele já tem.",
|
|
45
|
+
schema: gallerySchema,
|
|
46
|
+
handler: gallery,
|
|
47
|
+
},
|
|
48
|
+
sapiens_community: {
|
|
49
|
+
description: "Chat da comunidade Sapiens (assinantes + alumni). Sub-actions: list (últimas N mensagens da sala 'geral' por default), send (Claude posta como intercessor do user; sufixo '· via Claude' é adicionado pelo servidor), react (toggle emoji numa mensagem — allowlist 👍🔥❤️🚀🤯). Voz nas postagens deve seguir o DNA editorial Sapiens (anti-corporate, primeira pessoa, sem em-dash).",
|
|
50
|
+
schema: communitySchema,
|
|
51
|
+
handler: community,
|
|
52
|
+
},
|
|
53
|
+
sapiens_article: {
|
|
54
|
+
description: "CRUD direto de artigos do blog Sapiens (v1.1). Sub-actions: get (by slug, retorna doc completo pra edit local), update (patch em title/excerpt/tldr/content/tags/etc, NÃO toca status/column/format), publish (status='published', set publishedAt), unpublish (volta pra draft), delete (irreversível). Pra criar artigo novo use sapiens_quote_pop (quote ou pop) ou sapiens_pipeline action=create_draft_article_and_source (cru, vira source).",
|
|
55
|
+
schema: articleSchema,
|
|
56
|
+
handler: article,
|
|
57
|
+
},
|
|
58
|
+
sapiens_write: {
|
|
59
|
+
description: "Artigos self-serve do PRÓPRIO usuário (qualquer conta logada, não só admin) — espaço pessoal, aparece em /u/<username>, NÃO é o blog editorial. Sub-actions: generate (gera 1 artigo na voz Sapiens a partir de brief livre, ou reescrevendo um artigo publicado/texto teu; custa 400 Sinapses, reembolsa se falhar; salva como rascunho), list (teus artigos), get (1 artigo teu por id, corpo completo), update (edita title/content/excerpt/tldr), publish (publish=true publica no teu perfil, false volta pra rascunho). Identidade vem do sessionToken; cobra as Sinapses do dono do token. Pra blog editorial curado (owner-only) use sapiens_article.",
|
|
60
|
+
schema: writeSchema,
|
|
61
|
+
handler: write,
|
|
62
|
+
},
|
|
63
|
+
sapiens_quote_pop: {
|
|
64
|
+
description: "Publica curado da Coluna Sapiens (publish_quote), Coluna Repertório (publish_pop) ou Educativo (publish_educativo) via session token (v1.2). publish_quote: cria entry com column='sapiens', exige objeto quote completo (text/author/sourceWork/license/referenceImage/flowImage). publish_pop: cria entry com column='repertorio' format='pop-article', exige popReference{featuredItemId,relatedItemIds?,lensTheme?}. publish_educativo: cria artigo derivado de aula (Trilhas → Blog), exige educativeReference{sourceLessonId,angle?,partNumber?,totalParts?}. Default status='draft' (admin revisa em /dashboard/admin/...); publishNow=true publica direto.",
|
|
65
|
+
schema: quotePopSchema,
|
|
66
|
+
handler: quotePop,
|
|
67
|
+
},
|
|
68
|
+
sapiens_search: {
|
|
69
|
+
description: "Busca substring case-insensitive em title/excerpt/tldr/slug/tags dos artigos (v1.2). Filtros opcionais: column ('sapiens'/'repertorio'), format ('short'/'essay'/'pop-article'), status ('draft'/'published'/'archived'), tag exato. Default limit 30 (max 100). Use pra achar slug/id de artigo pré-existente antes de editar/publicar/deletar via sapiens_article.",
|
|
70
|
+
schema: searchSchema,
|
|
71
|
+
handler: search,
|
|
72
|
+
},
|
|
73
|
+
sapiens_studios: {
|
|
74
|
+
description: "Catálogo dos estúdios/experimentos Sapiens (v1.3, atualizado v1.4). Sub-actions: list (todos com URL+status+tags+mcpReady), get (detalhe de 1 slug), publishable_url (formata URL /articles/<slug>). Use quando user pergunta 'que estúdios existem'. Estúdios cobertos: helen-voice (v1.4 mcpReady), musicator (v1.4 mcpReady), persona-atlas (v1.4 mcpReady), sapiens-shorts, sapiens-video, cena-visual, text-post-builder, carrosel-editorial, comunidade, repertorio.",
|
|
75
|
+
schema: studiosSchema,
|
|
76
|
+
handler: studios,
|
|
77
|
+
},
|
|
78
|
+
sapiens_persona: {
|
|
79
|
+
description: "Persona Atlas — 16 arquétipos MBTI ilustrados (v1.4 + list_generated v1.5). Sub-actions: list_codes (16 codes + grupo NT/NF/SJ/SP, estático), list_generated (v1.5, query personaArtData.getAll, lê do banco quais já foram gerados com bunnyUrl + updatedAt), generate (qualquer logado, 450 Sinapses, retorna URL Bunny). Codes: INTJ/INTP/ENTJ/ENTP/INFJ/INFP/ENFJ/ENFP/ISTJ/ISFJ/ESTJ/ESFJ/ISTP/ISFP/ESTP/ESFP.",
|
|
80
|
+
schema: personaSchema,
|
|
81
|
+
handler: persona,
|
|
82
|
+
},
|
|
83
|
+
sapiens_helen: {
|
|
84
|
+
description: "Helen Voice TTS via ElevenLabs ou Google Gemini (qualquer logado; cobra Sinapses). Sub-actions: list_presets (catálogo de voiceIds/voiceNames recomendados + stylePreambles), speak (sintetiza, retorna audioBase64+mimeType+sizeBytes). BYOK suportado via clientApiKey, senão usa env do deploy. Custo: ElevenLabs ~$0.30/500c, Google ~$0.01/500c. Max 5000 chars (quebra antes em chunks). text pré-processado pelo caller (sem em-dash, sem markdown).",
|
|
85
|
+
schema: helenSchema,
|
|
86
|
+
handler: helen,
|
|
87
|
+
},
|
|
88
|
+
sapiens_musicator: {
|
|
89
|
+
description: "Musicator — letras e synth de música via voz Sapiens (qualquer logado; lyrics 300 / render 3000 Sinapses). Sub-actions: 'lyrics' (gera letra PT + stylePrompt EN inline, sem criar track, 300 sinapses), 'render' (v1.9, schedula synth Lyria/ACE/Suno num trackId existente, assíncrono fire-and-forget, 3000 sinapses, 3/min). Pra render: cria track no studio primeiro (UI Musicator), preenche lyrics+stylePrompt via /sapiens:lyrics ou direto, então chama render aqui. Cheque musicator_tracks.<trackId>.status pra acompanhar (rendering → completed/failed).",
|
|
90
|
+
schema: musicatorSchema,
|
|
91
|
+
handler: musicator,
|
|
92
|
+
},
|
|
93
|
+
sapiens_shorts: {
|
|
94
|
+
description: "Sapiens Shorts — render vertical 9:16 via VEO com brief structured (v1.5, admin-only). Sub-action: render. Args: imageId (persona pré-existente em generatedImages, descubra via sapiens_gallery), styleId ('ugc'/'unboxing'/'app-demo'/'reflexao'), brief (product+hook+shots+vibe), references opcionais. Retorna {success, url} (URL VEO com expiração curta — baixe logo). Pré-requisito: o imageId precisa ter row em generatedImages do user da sessão e cost definido. Pra criar row, use a UI primeiro (v1.6 vai cobrir requestGeneration via MCP).",
|
|
95
|
+
schema: shortsSchema,
|
|
96
|
+
handler: shorts,
|
|
97
|
+
},
|
|
98
|
+
sapiens_video: {
|
|
99
|
+
description: "Sapiens Video — render longer-form via VEO com prompt livre (v1.5, admin-only). Sub-action: generate. Args: imageId, prompt livre, aspectRatio opcional ('16:9'/'9:16'/'1:1'), references opcionais (start/end frames base64). Modelos definidos no row do imageId: sapiens-video-lite (2000 sinapses), -fast (5000), -quality (25000). Retorna {success, url}. Pré-requisito mesmo de sapiens_shorts: imageId já com row + cost.",
|
|
100
|
+
schema: videoSchema,
|
|
101
|
+
handler: video,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const server = new Server({ name: "mcp-sapiens", version: "1.7.0" }, { capabilities: { tools: {} } });
|
|
105
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
106
|
+
tools: Object.entries(TOOLS).map(([name, t]) => ({
|
|
107
|
+
name,
|
|
108
|
+
description: t.description,
|
|
109
|
+
inputSchema: zodToJsonSchema(t.schema),
|
|
110
|
+
})),
|
|
111
|
+
}));
|
|
112
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
113
|
+
const name = req.params.name;
|
|
114
|
+
const tool = TOOLS[name];
|
|
115
|
+
if (!tool) {
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: `Tool desconhecida: ${name}` }],
|
|
118
|
+
isError: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const args = tool.schema.parse(req.params.arguments ?? {});
|
|
123
|
+
const result = await tool.handler(args);
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: `Erro: ${e?.message ?? String(e)}` }],
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const transport = new StdioServerTransport();
|
|
136
|
+
await server.connect(transport);
|
|
137
|
+
console.error("mcp-sapiens v1.7.0 rodando via stdio (16 tools)");
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* CRUD direto de artigos do blog Sapiens via session token. Substitui o
|
|
5
|
+
* pattern antigo (admin-auth + scripts node). Sub-actions cobrem todo o
|
|
6
|
+
* ciclo de vida de um article publishado:
|
|
7
|
+
* - get: lê by slug (lê o documento completo pra edit local)
|
|
8
|
+
* - update: patch nos campos editáveis (title/excerpt/content/tags/etc)
|
|
9
|
+
* - publish: muda status="published", set publishedAt
|
|
10
|
+
* - unpublish: volta status="draft" (não apaga)
|
|
11
|
+
* - delete: apaga (irreversível, sem cleanup de tabelas relacionadas)
|
|
12
|
+
*
|
|
13
|
+
* Cria article novo via sapiens_quote_pop (quote ou pop) ou via
|
|
14
|
+
* sapiens_pipeline action=create_draft_article_and_source (artigo cru do
|
|
15
|
+
* blog que vira source pra fan-out). Este tool é só pra editar/publicar
|
|
16
|
+
* articles que JÁ existem.
|
|
17
|
+
*/
|
|
18
|
+
export const articleSchema = z.object({
|
|
19
|
+
action: z.enum([
|
|
20
|
+
"get",
|
|
21
|
+
"update",
|
|
22
|
+
"publish",
|
|
23
|
+
"unpublish",
|
|
24
|
+
"delete",
|
|
25
|
+
"ensure_visuals",
|
|
26
|
+
]),
|
|
27
|
+
slug: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Obrigatório pra action=get. Forma kebab-case."),
|
|
31
|
+
articleId: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Obrigatório pra update/publish/unpublish/delete/ensure_visuals. Pode descobrir via action=get (o retorno tem _id)."),
|
|
35
|
+
// Campos pra update (todos opcionais)
|
|
36
|
+
title: z.string().optional(),
|
|
37
|
+
excerpt: z.string().optional(),
|
|
38
|
+
tldr: z.string().optional(),
|
|
39
|
+
content: z.string().optional(),
|
|
40
|
+
tags: z.array(z.string()).optional(),
|
|
41
|
+
thumbnailUrl: z.string().optional(),
|
|
42
|
+
seoDescription: z.string().optional(),
|
|
43
|
+
readingTimeMinutes: z.number().optional(),
|
|
44
|
+
category: z.string().optional(),
|
|
45
|
+
connectedSlugs: z.array(z.string()).optional(),
|
|
46
|
+
// ensure_visuals: força regeneração de visuais específicos. Default é
|
|
47
|
+
// gerar APENAS o que falta (banner sem thumbnailUrl, inline sem bodyImages,
|
|
48
|
+
// conceptMap sem conceptMap). force* manda regerar mesmo se existe.
|
|
49
|
+
forceBanner: z
|
|
50
|
+
.boolean()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("ensure_visuals: regera banner mesmo se já existe."),
|
|
53
|
+
forceInline: z
|
|
54
|
+
.boolean()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("ensure_visuals: regera ilustrações inline mesmo se já existem."),
|
|
57
|
+
forceConceptMap: z
|
|
58
|
+
.boolean()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("ensure_visuals: regera mapa visual mesmo se já existe."),
|
|
61
|
+
inlineCount: z
|
|
62
|
+
.number()
|
|
63
|
+
.int()
|
|
64
|
+
.min(1)
|
|
65
|
+
.max(3)
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("ensure_visuals: quantas ilustrações inline gerar (1-3). Default 1."),
|
|
68
|
+
// Classifica retroativamente como Educativo (Trilhas → Blog).
|
|
69
|
+
educativeReference: z
|
|
70
|
+
.object({
|
|
71
|
+
sourceLessonId: z.string().describe("lessons:_id"),
|
|
72
|
+
sourceCourseSlug: z.string().optional(),
|
|
73
|
+
sourceModuleId: z.string().optional(),
|
|
74
|
+
angle: z.string().optional(),
|
|
75
|
+
partNumber: z.number().optional(),
|
|
76
|
+
totalParts: z.number().optional(),
|
|
77
|
+
})
|
|
78
|
+
.optional(),
|
|
79
|
+
});
|
|
80
|
+
function need(value, name) {
|
|
81
|
+
if (value === undefined || value === null) {
|
|
82
|
+
throw new Error(`Faltando arg "${name}" pra essa action.`);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
export async function article(args) {
|
|
87
|
+
const sessionToken = getSessionToken();
|
|
88
|
+
switch (args.action) {
|
|
89
|
+
case "get": {
|
|
90
|
+
const slug = need(args.slug, "slug");
|
|
91
|
+
const result = await convexQuery("mcpExtras:mcpGetArticleBySlug", {
|
|
92
|
+
sessionToken,
|
|
93
|
+
slug,
|
|
94
|
+
});
|
|
95
|
+
if (!result)
|
|
96
|
+
return { found: false, slug };
|
|
97
|
+
// Retorna documento completo (não-slim) — caller usa pra edit local.
|
|
98
|
+
return { found: true, article: result };
|
|
99
|
+
}
|
|
100
|
+
case "update": {
|
|
101
|
+
const articleId = need(args.articleId, "articleId");
|
|
102
|
+
const updated = await convexMutation("mcpExtras:mcpUpdateArticle", {
|
|
103
|
+
sessionToken,
|
|
104
|
+
articleId,
|
|
105
|
+
title: args.title,
|
|
106
|
+
excerpt: args.excerpt,
|
|
107
|
+
tldr: args.tldr,
|
|
108
|
+
content: args.content,
|
|
109
|
+
tags: args.tags,
|
|
110
|
+
thumbnailUrl: args.thumbnailUrl,
|
|
111
|
+
seoDescription: args.seoDescription,
|
|
112
|
+
readingTimeMinutes: args.readingTimeMinutes,
|
|
113
|
+
category: args.category,
|
|
114
|
+
connectedSlugs: args.connectedSlugs,
|
|
115
|
+
educativeReference: args.educativeReference,
|
|
116
|
+
});
|
|
117
|
+
return { ok: true, article: updated };
|
|
118
|
+
}
|
|
119
|
+
case "publish": {
|
|
120
|
+
const articleId = need(args.articleId, "articleId");
|
|
121
|
+
return await convexMutation("mcpExtras:mcpPublishArticle", {
|
|
122
|
+
sessionToken,
|
|
123
|
+
articleId,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
case "unpublish": {
|
|
127
|
+
const articleId = need(args.articleId, "articleId");
|
|
128
|
+
return await convexMutation("mcpExtras:mcpUnpublishArticle", {
|
|
129
|
+
sessionToken,
|
|
130
|
+
articleId,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
case "delete": {
|
|
134
|
+
const articleId = need(args.articleId, "articleId");
|
|
135
|
+
return await convexMutation("mcpExtras:mcpDeleteArticle", {
|
|
136
|
+
sessionToken,
|
|
137
|
+
articleId,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
case "ensure_visuals": {
|
|
141
|
+
// Gera banner / inline / conceptMap que estiverem faltando.
|
|
142
|
+
// Idempotente: pula o que já existe. Custo: ~1700 sinapses pra
|
|
143
|
+
// artigo novo (450+450+800), só do que regerar pros que existem.
|
|
144
|
+
const articleId = need(args.articleId, "articleId");
|
|
145
|
+
return await convexAction("articleVisuals:ensureArticleVisualsBySession", {
|
|
146
|
+
sessionToken,
|
|
147
|
+
articleId,
|
|
148
|
+
forceBanner: args.forceBanner,
|
|
149
|
+
forceInline: args.forceInline,
|
|
150
|
+
forceConceptMap: args.forceConceptMap,
|
|
151
|
+
inlineCount: args.inlineCount,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, getSessionToken } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Comunidade Sapiens — chat compartilhado entre assinantes/alumni.
|
|
5
|
+
* Wrapper sobre `desktopMcp.communityList` / `communitySend` / `communityReact`.
|
|
6
|
+
*
|
|
7
|
+
* Claude posta como intercessor do user. Toda mensagem ganha sufixo " · via Claude"
|
|
8
|
+
* pra transparência (regra do servidor, não pode ser removida).
|
|
9
|
+
*
|
|
10
|
+
* Gate de acesso: o paywall do servidor valida assinatura/alumni. Se o user
|
|
11
|
+
* não tem acesso, list devolve vazio e send/react retornam erro claro.
|
|
12
|
+
*/
|
|
13
|
+
export const communitySchema = z.object({
|
|
14
|
+
action: z.enum(["list", "send", "react"]),
|
|
15
|
+
roomSlug: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Default 'geral'. Pra criar sala nova precisa do admin no site."),
|
|
19
|
+
limit: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.positive()
|
|
23
|
+
.max(100)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Pra action=list. Default 30, max 100."),
|
|
26
|
+
content: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Texto da mensagem (action=send). Max ~487 chars (suffix '· via Claude' adicionado pelo servidor). Use voz Sapiens."),
|
|
30
|
+
replyTo: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("communityChatMessages:_id da mensagem que está respondendo (action=send opcional)."),
|
|
34
|
+
messageId: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("communityChatMessages:_id (obrigatório pra action=react)."),
|
|
38
|
+
emoji: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Emoji da reação (action=react). Allowlist do server: 👍 🔥 ❤️ 🚀 🤯"),
|
|
42
|
+
});
|
|
43
|
+
export async function community(args) {
|
|
44
|
+
const sessionToken = getSessionToken();
|
|
45
|
+
if (args.action === "list") {
|
|
46
|
+
return await convexAction("desktopMcp:communityList", {
|
|
47
|
+
sessionToken,
|
|
48
|
+
roomSlug: args.roomSlug,
|
|
49
|
+
limit: args.limit,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (args.action === "send") {
|
|
53
|
+
if (!args.content || !args.content.trim()) {
|
|
54
|
+
throw new Error("action=send exige content (texto não-vazio).");
|
|
55
|
+
}
|
|
56
|
+
return await convexAction("desktopMcp:communitySend", {
|
|
57
|
+
sessionToken,
|
|
58
|
+
content: args.content,
|
|
59
|
+
roomSlug: args.roomSlug,
|
|
60
|
+
replyTo: args.replyTo,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (args.action === "react") {
|
|
64
|
+
if (!args.messageId)
|
|
65
|
+
throw new Error("action=react exige messageId.");
|
|
66
|
+
if (!args.emoji)
|
|
67
|
+
throw new Error("action=react exige emoji.");
|
|
68
|
+
return await convexAction("desktopMcp:communityReact", {
|
|
69
|
+
sessionToken,
|
|
70
|
+
messageId: args.messageId,
|
|
71
|
+
emoji: args.emoji,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, getSessionToken } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read-only do gallery do usuário desktop (imagens geradas via nanoBanana).
|
|
5
|
+
* Wrapper sobre `desktopMcp.galleryList` / `desktopMcp.galleryGet`.
|
|
6
|
+
*
|
|
7
|
+
* Usado pra:
|
|
8
|
+
* - Listar imagens recentes do user (pra reusar como referenceImage)
|
|
9
|
+
* - Pegar bytes de uma imagem pra mostrar inline no Claude
|
|
10
|
+
* - Descobrir imageId pra passar como sourceImageId em sapiens_image edit/variation
|
|
11
|
+
*/
|
|
12
|
+
export const gallerySchema = z.object({
|
|
13
|
+
action: z.enum(["list", "get"]),
|
|
14
|
+
limit: z
|
|
15
|
+
.number()
|
|
16
|
+
.int()
|
|
17
|
+
.positive()
|
|
18
|
+
.max(100)
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Default 20. Max 100. Só pra action=list."),
|
|
21
|
+
model: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Filtro de model (action=list). Ex: 'nano-banana-max' pra ver só Pro."),
|
|
25
|
+
imageId: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("generatedImages:_id (obrigatório pra action=get)"),
|
|
29
|
+
includeBase64: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Default false. Quando true, action=get devolve os bytes em base64 (pesado, evite em listagens)."),
|
|
33
|
+
});
|
|
34
|
+
export async function gallery(args) {
|
|
35
|
+
const sessionToken = getSessionToken();
|
|
36
|
+
if (args.action === "list") {
|
|
37
|
+
const result = await convexAction("desktopMcp:galleryList", {
|
|
38
|
+
sessionToken,
|
|
39
|
+
limit: args.limit ?? 20,
|
|
40
|
+
model: args.model,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
count: result?.items?.length ?? 0,
|
|
44
|
+
items: result?.items ?? [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (args.action === "get") {
|
|
48
|
+
if (!args.imageId) {
|
|
49
|
+
throw new Error("action=get exige imageId.");
|
|
50
|
+
}
|
|
51
|
+
return await convexAction("desktopMcp:galleryGet", {
|
|
52
|
+
sessionToken,
|
|
53
|
+
imageId: args.imageId,
|
|
54
|
+
includeBase64: args.includeBase64 ?? false,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|