sapiens-mcp 1.15.0 → 1.17.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/dist/index.js CHANGED
@@ -31,17 +31,17 @@ const TOOLS = {
31
31
  handler: pipeline,
32
32
  },
33
33
  sapiens_image: {
34
- description: "Operações de imagem via Sapiens (Gemini, Azure gpt-image-2, Grok, Veo). Sub-actions: 'generate' (gera imagem completa imediato — prompt+model+aspectRatio+size; suporta mode=edit/variation e MULTI-REFERÊNCIA: combine até 4 imagens como referência numa geração só, igual ao modal 'Selecionar Referência' do web — via referenceImageUrls (sua galeria + Acervo + personagens públicos de sapiens_character) e/ou sourceImageIds (ids da sua galeria); refs valem pros modelos robustos nano-banana-2/gpt-image-2-*/grok-2-image*), '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.",
34
+ description: "Operações de imagem via Sapiens (Gemini, Azure gpt-image-2, Grok, Veo). Sub-actions: 'generate' (gera imagem completa imediato — prompt+model+aspectRatio+size; suporta mode=edit/variation e MULTI-REFERÊNCIA: combine até 4 imagens como referência numa geração só, igual ao modal 'Selecionar Referência' do web — via referenceImageUrls (sua galeria + Acervo + personagens públicos de sapiens_character) e/ou sourceImageIds (ids da sua galeria); refs valem pros modelos robustos nano-banana-2/gpt-image-2-*/grok-2-image*), '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. TEMPLATE (v1.17): passe templateSlug numa generate pra usar um super-prompt travado da casa — o `prompt` vira só a CENA (quem + pose + objeto-conceito) e o template embrulha estilo+fundo+enquadramento+ref de traço. 'retrato-sapiens-v1' = retrato editorial cartoon de um personagem no grid verde Sapiens (mesma 'mão' dos artigos); sem ref própria, injeta a Helen como âncora de traço (passar referenceImageUrls troca quem aparece). Mutuamente exclusivo com brandSlug.",
35
35
  schema: imageSchema,
36
36
  handler: image,
37
37
  },
38
38
  sapiens_meta: {
39
- 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.",
39
+ 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 (inclui a versão do MCP), version (qual versão do sapiens-mcp está REALMENTE rodando + se é a última do npm; não exige login; use pra saber se o client pegou a versão nova ou ficou preso em cache do npx), app_url (URLs canônicas). Use credits/subscription antes de gerar imagem pra avisar se vai estourar.",
40
40
  schema: metaSchema,
41
41
  handler: meta,
42
42
  },
43
43
  sapiens_repertorio: {
44
- description: "Acervo pessoal de filme/série/anime/jogo/livro/música (Repertório, o segundo cérebro do user). Reads: list (filtros mediaType/status), search (texto em title/genres/tags), get (detalhe), lists (listas curadas), popArticles, resolve (busca capa/ano/id nos providers server-side: OMDb/IGDB-Twitch/AniList/Google Books/iTunes). Mutations (qualquer logado, mexem no PRÓPRIO acervo): add_item, update_item (status/rating/tags/note/isPublic), remove_item. CAPTURA ONE-SHOT: quando o user fala natural ('acabei de ver Duna 2, nota 9', 'tô jogando Hollow Knight', 'li tal livro'), faça add_item SEM questionário: infira mediaType e status (assisti/zerei/li=completed, quero=backlog, tô jogando/vendo=active, dropei=dropped), pegue rating se citado; (2) chame action=resolve {mediaType, query} pra pegar capa+ano+externalId e grave add_item com o source do candidato (twitch/anilist/googlebooks/itunes, ou manual+imdbID pra filme/série do OMDb) + posterUrl; (3) se resolve vier providerKeyMissing ou vazio, grave source='manual' + UUID em externalId com title/year/genres/overview que você conhece. Upsert/dedup por (userId, source, externalId). Só pergunte se o título for ambíguo.",
44
+ description: "Acervo pessoal de filme/série/anime/jogo/livro/música (Repertório, o segundo cérebro do user). Reads: list (filtros mediaType/status), search (texto em title/genres/tags), get (detalhe), lists (listas curadas), popArticles, resolve (busca capa/ano/id nos providers server-side: OMDb/IGDB-Twitch/AniList/Google Books/iTunes). Mutations (qualquer logado, mexem no PRÓPRIO acervo): add_item, update_item (status/rating/tags/note/isPublic), remove_item. CAPTURA ONE-SHOT travada na lista de providers: quando o user fala natural ('acabei de ver Duna 2, nota 9', 'tô jogando Hollow Knight', 'li tal livro'), (1) infira mediaType e status (assisti/zerei/li=completed, quero=backlog, tô jogando/vendo=active, dropei=dropped) e rating se citado; (2) chame action=resolve {mediaType, query}, escolha o candidato certo e faça add_item passando SÓ o source + externalId DELE + os campos pessoais (status/rating/tags/note). O servidor re-resolve no provider e grava título/capa/ano canônicos — você NÃO manda título/capa nem inventa externalId. (3) Se o resolve não achar (lista vazia/providerKeyMissing), NÃO dá pra adicionar: diga ao user que não encontrou nos providers (não fabrique entry manual). Upsert/dedup por (userId, source, externalId). Só pergunte se ambíguo entre candidatos.",
45
45
  schema: repertorioSchema,
46
46
  handler: repertorio,
47
47
  },
@@ -126,7 +126,7 @@ const TOOLS = {
126
126
  handler: profile,
127
127
  },
128
128
  };
129
- const server = new Server({ name: "mcp-sapiens", version: "1.15.0" }, { capabilities: { tools: {} } });
129
+ const server = new Server({ name: "mcp-sapiens", version: "1.17.0" }, { capabilities: { tools: {} } });
130
130
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
131
131
  tools: Object.entries(TOOLS).map(([name, t]) => ({
132
132
  name,
@@ -159,4 +159,4 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
159
159
  });
160
160
  const transport = new StdioServerTransport();
161
161
  await server.connect(transport);
162
- console.error("mcp-sapiens v1.15.0 rodando via stdio (20 tools)");
162
+ console.error("mcp-sapiens v1.17.0 rodando via stdio (20 tools)");
@@ -57,6 +57,10 @@ export const imageSchema = z.object({
57
57
  .enum(["persona", "logo", "none"])
58
58
  .optional()
59
59
  .describe("Marca na imagem do brand: 'persona' (personagem do brand via character sheets, ex: Helen), 'logo' (carimba a logo no canto), 'none' (só o estilo). Default 'none'. Só aplica se brandSlug setado e o brand oferecer a marca."),
60
+ templateSlug: z
61
+ .string()
62
+ .optional()
63
+ .describe("Slug de um template de imagem (super-prompt travado da casa). Quando setado, o `prompt` vira só a CENA (quem + pose + objeto-conceito) e o template embrulha com o estilo + fundo + enquadramento + ref de traço da casa. Ex: 'retrato-sapiens-v1' = retrato editorial cartoon de UM personagem no grid verde Sapiens (mesma 'mão' dos artigos). O template define model/aspect/size default (sobreponíveis) e injeta a ref da Helen como âncora de traço quando você não passa referenceImageUrls própria (passar refs = trocar quem aparece, mantendo o estilo). Mutuamente exclusivo com brandSlug. Vale em action=generate, mode=create."),
60
64
  mode: z
61
65
  .enum(["create", "edit", "variation"])
62
66
  .optional()
@@ -148,7 +152,9 @@ export async function image(args) {
148
152
  prompt: args.prompt,
149
153
  aspectRatio: args.aspectRatio,
150
154
  size: args.size,
151
- model: args.model ?? "nano-banana-2",
155
+ // Com templateSlug, deixa o model em branco pro default do template valer
156
+ // (sem template, mantém o default nano-banana-2 da tool).
157
+ model: args.model ?? (args.templateSlug ? undefined : "nano-banana-2"),
152
158
  negativePrompt: args.negativePrompt,
153
159
  mode: args.mode ?? "create",
154
160
  sourceImageId: args.sourceImageId,
@@ -156,6 +162,7 @@ export async function image(args) {
156
162
  referenceImageUrls: args.referenceImageUrls,
157
163
  brandSlug: args.brandSlug,
158
164
  brandMark: args.brandMark,
165
+ templateSlug: args.templateSlug,
159
166
  });
160
167
  return {
161
168
  imageId: result?.imageId,
@@ -1,5 +1,25 @@
1
1
  import { z } from "zod";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
2
5
  import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, describeConvexError, } from "../convexClient.js";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ /**
8
+ * Lê o `version` do package.json do PRÓPRIO binário em runtime. Reflete o que
9
+ * está REALMENTE rodando (não um valor cravado no código nem server-side), então
10
+ * serve pra flagrar client preso em cache antigo do npx. dist/tools/meta.js ->
11
+ * ../../package.json (raiz do pacote); em dev (src/tools) o caminho bate igual.
12
+ */
13
+ function readMcpVersion() {
14
+ try {
15
+ const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
16
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
17
+ return typeof pkg?.version === "string" ? pkg.version : "unknown";
18
+ }
19
+ catch {
20
+ return "unknown";
21
+ }
22
+ }
3
23
  export const metaSchema = z.object({
4
24
  action: z.enum([
5
25
  "login",
@@ -10,6 +30,7 @@ export const metaSchema = z.object({
10
30
  "credits",
11
31
  "app_url",
12
32
  "subscription",
33
+ "version",
13
34
  ]),
14
35
  code: z
15
36
  .string()
@@ -123,6 +144,45 @@ export async function meta(args) {
123
144
  adminPipeline: `${APP_URL}/dashboard/admin/content`,
124
145
  };
125
146
  }
147
+ // version: qual versão do sapiens-mcp está rodando AGORA (lida do package.json
148
+ // do binário) + compara com a última do npm (best-effort, com timeout). Não
149
+ // exige login: é sobre o binário, não a conta. É o teste limpo pra saber se o
150
+ // client pegou a versão nova ou ficou preso em cache do npx.
151
+ if (args.action === "version") {
152
+ const version = readMcpVersion();
153
+ let latest = null;
154
+ let upToDate = null;
155
+ try {
156
+ const ctrl = new AbortController();
157
+ const timer = setTimeout(() => ctrl.abort(), 2500);
158
+ const resp = await fetch("https://registry.npmjs.org/sapiens-mcp/latest", {
159
+ signal: ctrl.signal,
160
+ });
161
+ clearTimeout(timer);
162
+ if (resp.ok) {
163
+ const data = await resp.json();
164
+ if (typeof data?.version === "string") {
165
+ latest = data.version;
166
+ upToDate = latest === version;
167
+ }
168
+ }
169
+ }
170
+ catch {
171
+ // offline / timeout: reporta só a versão local (ainda útil)
172
+ }
173
+ return {
174
+ name: "sapiens-mcp",
175
+ version,
176
+ latest,
177
+ upToDate,
178
+ runtime: `node ${process.version}`,
179
+ note: latest && upToDate === false
180
+ ? `Rodando ${version}, mas a última no npm é ${latest}: o client está ATRASADO (provável cache do npx). Pra atualizar: pinar sapiens-mcp@${latest} na config do MCP ou recriar o container (restart sozinho não basta).`
181
+ : latest && upToDate
182
+ ? `Na última versão (${version}).`
183
+ : `Versão do binário rodando agora: ${version}. Não consegui consultar o npm pra comparar (offline/timeout).`,
184
+ };
185
+ }
126
186
  // Login: troca o código de uso único (gerado no site, logado) por um
127
187
  // sessionToken de 30 dias e salva localmente. NÃO exige token prévio.
128
188
  if (args.action === "login") {
@@ -243,6 +303,7 @@ export async function meta(args) {
243
303
  }
244
304
  return {
245
305
  ok: true,
306
+ version: readMcpVersion(),
246
307
  tier: isAdmin ? "admin" : "user",
247
308
  email: sub?.user?.email ?? null,
248
309
  balance: sub?.balance ?? null,
@@ -11,6 +11,14 @@ import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../
11
11
  * PRÓPRIO acervo. Ownership: tudo vai pro userId da sessão (o user logado).
12
12
  * Caller NÃO escolhe pra quem adicionar.
13
13
  *
14
+ * add_item é TRAVADO na lista de providers (anti-fabricação): você passa só
15
+ * mediaType + source + externalId vindos de um `resolve` anterior + campos
16
+ * pessoais (status/rating/tags/note/isPublic). O servidor RE-RESOLVE no
17
+ * provider por id e grava o metadado canônico (título/capa/ano). Não dá pra
18
+ * inventar título/capa nem criar entry "manual" à mão — externalId que não
19
+ * resolve é rejeitado. Fluxo: resolve → escolhe candidato → add_item com o
20
+ * source+externalId dele.
21
+ *
14
22
  * Action-based design (memoria: consolidar tools via args).
15
23
  */
16
24
  const mediaTypeEnum = z.enum(["movie", "series", "anime", "game", "book", "music"]);
@@ -47,14 +55,14 @@ export const repertorioSchema = z.object({
47
55
  mediaType: mediaTypeEnum.optional(),
48
56
  status: statusEnum.optional(),
49
57
  limit: z.number().int().positive().max(500).optional().default(100),
50
- // add_item — todos os campos do schema repertorioItems user-side
58
+ // add_item — só identifica o item; o servidor re-resolve e grava o metadado.
51
59
  source: sourceEnum
52
60
  .optional()
53
- .describe("Pra add_item: 'tmdb'/'anilist'/'rawg' se vem de provider externo; 'manual' pra entry hand-rolled (gere externalId único via UUID)."),
61
+ .describe("Pra add_item: o source EXATO de um candidato do action=resolve (manual=filme/série via OMDb, anilist, twitch=jogo, googlebooks, itunes). Não invente."),
54
62
  externalId: z
55
63
  .string()
56
64
  .optional()
57
- .describe("Pra add_item: ID externo (tmdb/anilist/rawg) ou UUID se source='manual'. Dedup por (userId, source, externalId)."),
65
+ .describe("Pra add_item: o externalId EXATO do candidato do resolve (imdbID p/ manual, id do provider p/ resto). NÃO fabrique/UUID — id que não resolve no provider é rejeitado. Dedup por (userId, source, externalId)."),
58
66
  title: z.string().optional(),
59
67
  titleOriginal: z.string().optional(),
60
68
  year: z.number().optional(),
@@ -127,33 +135,20 @@ export async function repertorio(args) {
127
135
  args.action === "remove_item") {
128
136
  const sessionToken = getSessionToken();
129
137
  if (args.action === "add_item") {
130
- return await convexMutation("mcpExtras:mcpAddRepertorioItem", {
138
+ // Travado na lista de providers: manda só o identificador (source +
139
+ // externalId de um resolve) + campos pessoais. O servidor re-resolve no
140
+ // provider e grava título/capa/ano canônicos; título/capa do caller são
141
+ // ignorados de propósito (anti-fabricação).
142
+ return await convexAction("repertorioResolve:addResolvedItem", {
131
143
  sessionToken,
132
144
  mediaType: need(args.mediaType, "mediaType"),
133
145
  source: need(args.source, "source"),
134
146
  externalId: need(args.externalId, "externalId"),
135
- title: need(args.title, "title"),
136
- titleOriginal: args.titleOriginal,
137
- year: args.year,
138
- posterUrl: args.posterUrl,
139
- backdropUrl: args.backdropUrl,
140
- overview: args.overview,
141
- genres: args.genres,
142
- runtimeMin: args.runtimeMin,
143
- platforms: args.platforms,
144
- studios: args.studios,
145
- authors: args.authors,
146
- pageCount: args.pageCount,
147
- isbn: args.isbn,
148
147
  status: args.status,
149
148
  rating: args.rating === null ? undefined : args.rating,
150
149
  tags: args.tags,
151
150
  note: args.note,
152
151
  isPublic: args.isPublic,
153
- imdbId: args.imdbId,
154
- tmdbId: args.tmdbId,
155
- traktId: args.traktId,
156
- anilistId: args.anilistId,
157
152
  });
158
153
  }
159
154
  if (args.action === "update_item") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "MCP server pra operar o Sapiens Sintéticos (sapiensinteticos.com) pelo Claude Code: gerar imagem, escrever artigo, voz, música e mais, na sua conta. Login pelo código de sapiensinteticos.com/conectar-claude.",
5
5
  "type": "module",
6
6
  "bin": {