beplus-mcp 0.8.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 (3) hide show
  1. package/README.md +125 -0
  2. package/dist/index.js +1238 -0
  3. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,1238 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/config.ts
7
+ var DEFAULT_BASE_URL = "https://api.beplus.academy";
8
+ function loadConfig() {
9
+ const token = process.env.BEPLUS_API_TOKEN?.trim();
10
+ if (!token) {
11
+ throw new Error(
12
+ "BEPLUS_API_TOKEN is required. Generate a Personal Access Token in your BePlus account settings (Tokens de acesso) and set it as BEPLUS_API_TOKEN in the MCP config."
13
+ );
14
+ }
15
+ const baseUrl = (process.env.BEPLUS_API_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
16
+ const inlineImages = process.env.BEPLUS_INLINE_IMAGES !== "0";
17
+ const verifyOnStart = process.env.BEPLUS_VERIFY_ON_START === "1";
18
+ const rawThreshold = process.env.BEPLUS_COST_WARN_THRESHOLD;
19
+ const threshold = rawThreshold ? Number(rawThreshold) : NaN;
20
+ const activeProject = process.env.BEPLUS_ACTIVE_PROJECT?.trim() || null;
21
+ return {
22
+ baseUrl,
23
+ apiBase: `${baseUrl}/api/v1/mcp`,
24
+ token,
25
+ inlineImages,
26
+ inlineImageMax: 4,
27
+ inlineImageMaxBytes: 5 * 1024 * 1024,
28
+ verifyOnStart,
29
+ costWarnThreshold: Number.isFinite(threshold) && threshold > 0 ? threshold : null,
30
+ activeProject
31
+ };
32
+ }
33
+
34
+ // src/server.ts
35
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
36
+
37
+ // src/api/errors.ts
38
+ var ApiError = class extends Error {
39
+ httpStatus;
40
+ body;
41
+ constructor(httpStatus, message, body) {
42
+ super(message);
43
+ this.name = "ApiError";
44
+ this.httpStatus = httpStatus;
45
+ this.body = body;
46
+ }
47
+ };
48
+ function apiErrorToUserMessage(err) {
49
+ if (err instanceof ApiError) {
50
+ const detail = err.message || "";
51
+ switch (err.httpStatus) {
52
+ case 401:
53
+ return "Token inv\xE1lido ou expirado. Gere um novo Personal Access Token no painel BePlus e atualize BEPLUS_API_TOKEN no config do MCP.";
54
+ case 402:
55
+ return `Diamantes insuficientes. ${detail} Recarregue ou escolha um modelo/resolu\xE7\xE3o mais barato (use estimate_cost / list_models).`;
56
+ case 403:
57
+ return `Sem permiss\xE3o para esta a\xE7\xE3o. ${detail}`;
58
+ case 404:
59
+ return `N\xE3o encontrado. ${detail}`;
60
+ case 408:
61
+ return detail || "Tempo esgotado ao falar com a API BePlus.";
62
+ case 409:
63
+ return detail;
64
+ case 429:
65
+ return `Limite atingido. ${detail} Aguarde alguns segundos e tente novamente (reduza gera\xE7\xF5es em paralelo).`;
66
+ case 400:
67
+ case 422:
68
+ return `Par\xE2metros inv\xE1lidos: ${detail}. Use list_models para ver modelos/params v\xE1lidos.`;
69
+ case 0:
70
+ return detail || "Falha de rede ao falar com a API BePlus. Confira BEPLUS_API_URL.";
71
+ default:
72
+ if (err.httpStatus >= 500) return `Erro no servidor BePlus: ${detail}`;
73
+ return detail || `Erro HTTP ${err.httpStatus}`;
74
+ }
75
+ }
76
+ if (err instanceof Error) return err.message;
77
+ return String(err);
78
+ }
79
+
80
+ // src/api/client.ts
81
+ var BeplusClient = class {
82
+ constructor(cfg) {
83
+ this.cfg = cfg;
84
+ }
85
+ cfg;
86
+ async request(method, path, opts = {}) {
87
+ const url = new URL(`${this.cfg.apiBase}${path}`);
88
+ if (opts.query) {
89
+ for (const [k, v] of Object.entries(opts.query)) {
90
+ if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v));
91
+ }
92
+ }
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 3e4);
95
+ let res;
96
+ try {
97
+ res = await fetch(url, {
98
+ method,
99
+ headers: {
100
+ Authorization: `Bearer ${this.cfg.token}`,
101
+ ...opts.body ? { "Content-Type": "application/json" } : {}
102
+ },
103
+ body: opts.body ? JSON.stringify(opts.body) : void 0,
104
+ signal: controller.signal
105
+ });
106
+ } catch (e) {
107
+ if (e?.name === "AbortError") {
108
+ throw new ApiError(408, `Tempo esgotado ao falar com a API BePlus (${path}).`);
109
+ }
110
+ throw new ApiError(0, `Falha de rede ao falar com a API BePlus (${path}). Confira BEPLUS_API_URL.`);
111
+ } finally {
112
+ clearTimeout(timer);
113
+ }
114
+ const text = await res.text();
115
+ let json;
116
+ if (text) {
117
+ try {
118
+ json = JSON.parse(text);
119
+ } catch {
120
+ }
121
+ }
122
+ if (!res.ok) {
123
+ const message = json?.message || (Array.isArray(json?.error?.formatted) ? json.error.formatted.join(", ") : void 0) || res.statusText || `HTTP ${res.status}`;
124
+ throw new ApiError(res.status, message, json);
125
+ }
126
+ return json && typeof json === "object" && "result" in json ? json.result : json;
127
+ }
128
+ startGeneration(body) {
129
+ return this.request("POST", "/generations", { body });
130
+ }
131
+ listProjects() {
132
+ return this.request("GET", "/projects");
133
+ }
134
+ createProject(body) {
135
+ return this.request("POST", "/projects", { body });
136
+ }
137
+ getActiveProject() {
138
+ return this.request("GET", "/projects/active");
139
+ }
140
+ setActiveProject(project) {
141
+ return this.request("PUT", "/projects/active", { body: { project } });
142
+ }
143
+ // ---------------------------- clients ----------------------------
144
+ listClients() {
145
+ return this.request("GET", "/clients");
146
+ }
147
+ createClient(body) {
148
+ return this.request("POST", "/clients", { body });
149
+ }
150
+ setActiveClient(client) {
151
+ return this.request("PUT", "/clients/active", { body: { client } });
152
+ }
153
+ getStatus(id) {
154
+ return this.request("GET", `/generations/${id}`, { timeoutMs: 15e3 });
155
+ }
156
+ cancel(id) {
157
+ return this.request("DELETE", `/generations/${id}`);
158
+ }
159
+ listGenerations(query) {
160
+ return this.request("GET", "/generations", { query });
161
+ }
162
+ tts(body) {
163
+ return this.request("POST", "/tts", { body, timeoutMs: 9e4 });
164
+ }
165
+ models() {
166
+ return this.request("GET", "/models");
167
+ }
168
+ calculateCost(query) {
169
+ return this.request("GET", "/calculate-cost", { query });
170
+ }
171
+ balance() {
172
+ return this.request("GET", "/balance");
173
+ }
174
+ me() {
175
+ return this.request("GET", "/me");
176
+ }
177
+ readProvenance(body) {
178
+ return this.request("POST", "/read-provenance", { body, timeoutMs: 3e4 });
179
+ }
180
+ analyzeMedia(body) {
181
+ return this.request("POST", "/analyze-media", { body, timeoutMs: 3e5 });
182
+ }
183
+ searchCalls(query) {
184
+ return this.request("GET", "/calls", { query });
185
+ }
186
+ getCall(id) {
187
+ return this.request("GET", `/calls/${id}`);
188
+ }
189
+ };
190
+
191
+ // src/tools/generation.ts
192
+ import { z } from "zod";
193
+
194
+ // src/tools/team-gating.ts
195
+ var TEAM_GATING_NOTE = " \u26A0\uFE0F DESTINO DA GERA\xC7\xC3O: toda imagem/v\xEDdeo vai para o CLIENTE + PROJETO ATIVOS. ANTES de gerar, SEMPRE confirme com o usu\xE1rio em qual cliente e projeto trabalhar \u2014 MESMO que j\xE1 exista um projeto ativo, ele pode ter ficado de uma sess\xE3o anterior (N\xC3O confie no ativo herdado). Fluxo: chame list_clients e list_projects, mostre o ativo atual + as op\xE7\xF5es, e pe\xE7a pro usu\xE1rio escolher/confirmar (set_active_client + set_active_project, ou passe `project` na tool). Pode criar novos com create_client / create_project. S\xF3 gere depois que o usu\xE1rio confirmar o cliente+projeto de destino \u2014 nunca gere no ativo herdado sem confirmar.";
196
+ function isTeamGatingError(msg) {
197
+ return !!msg && /cliente e um projeto|selecione um cliente|sem cliente vinculado|antes de gerar/i.test(msg);
198
+ }
199
+ async function buildPickProjectMessage(client) {
200
+ let clientsTxt = " (n\xE3o foi poss\xEDvel listar clientes)";
201
+ let projectsTxt = " (n\xE3o foi poss\xEDvel listar projetos)";
202
+ try {
203
+ const { clients } = await client.listClients();
204
+ clientsTxt = clients.length ? clients.map((c) => ` \u2022 ${c.code} \u2014 ${c.name}`).join("\n") : " (nenhum cliente ainda \u2014 crie com create_client)";
205
+ } catch {
206
+ }
207
+ try {
208
+ const { projects } = await client.listProjects();
209
+ projectsTxt = projects.length ? projects.map(
210
+ (p) => ` \u2022 ${p.code} \u2014 ${p.name}` + (p.client_name ? ` (cliente: ${p.client_name})` : " (\u26A0\uFE0F sem cliente \u2014 vincule um antes)")
211
+ ).join("\n") : " (nenhum projeto ainda \u2014 crie com create_project)";
212
+ } catch {
213
+ }
214
+ return `\u26A0\uFE0F Esta conta \xE9 de EQUIPE: gerar exige um CLIENTE e um PROJETO ativos. Nada foi gerado ainda.
215
+ Pergunte ao usu\xE1rio em qual cliente/projeto vincular ANTES de gerar.
216
+
217
+ Clientes dispon\xEDveis:
218
+ ${clientsTxt}
219
+
220
+ Projetos dispon\xEDveis:
221
+ ${projectsTxt}
222
+
223
+ Para prosseguir: set_active_client + set_active_project (ou passe \`project=<code>\` no generate). Se o projeto escolhido estiver sem cliente, vincule um primeiro (no admin/IA Lab).`;
224
+ }
225
+
226
+ // src/poll.ts
227
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
228
+ async function pollUntilDone(client, generationId, budgetMs, jitterSeed = 0) {
229
+ const start = Date.now();
230
+ let interval = 1500;
231
+ let last;
232
+ let tick = 0;
233
+ while (Date.now() - start < budgetMs) {
234
+ const status = await client.getStatus(generationId);
235
+ last = status;
236
+ if (status.status === "completed" || status.status === "failed") {
237
+ return { settled: true, status };
238
+ }
239
+ const jitter = (jitterSeed + tick) % 5 * 100 - 250;
240
+ await sleep(Math.max(500, interval + jitter));
241
+ interval = Math.min(Math.round(interval * 1.4), 5e3);
242
+ tick += 1;
243
+ }
244
+ if (!last) last = await client.getStatus(generationId);
245
+ return { settled: last.status === "completed" || last.status === "failed", status: last };
246
+ }
247
+
248
+ // src/log.ts
249
+ function fmt(a) {
250
+ return typeof a === "string" ? a : JSON.stringify(a);
251
+ }
252
+ function logInfo(...args) {
253
+ process.stderr.write(`[beplus] ${args.map(fmt).join(" ")}
254
+ `);
255
+ }
256
+ function logError(...args) {
257
+ process.stderr.write(`[beplus][error] ${args.map(fmt).join(" ")}
258
+ `);
259
+ }
260
+
261
+ // src/format.ts
262
+ function textResult(s) {
263
+ return { content: [{ type: "text", text: s }] };
264
+ }
265
+ function errorResult(s) {
266
+ return { content: [{ type: "text", text: s }], isError: true };
267
+ }
268
+ function compact(obj) {
269
+ const out = {};
270
+ for (const [k, v] of Object.entries(obj)) {
271
+ if (v !== void 0 && v !== null) out[k] = v;
272
+ }
273
+ return out;
274
+ }
275
+ function costFooter(start, cfg) {
276
+ const parts = [];
277
+ if (typeof start.diamond_cost === "number") parts.push(`Custo: ${start.diamond_cost} \u{1F48E}`);
278
+ if (typeof start.new_balance === "number") parts.push(`Saldo: ${start.new_balance} \u{1F48E}`);
279
+ let footer = parts.join(" \xB7 ");
280
+ if (cfg.costWarnThreshold && typeof start.diamond_cost === "number" && start.diamond_cost > cfg.costWarnThreshold) {
281
+ footer = `\u26A0 Custo alto (${start.diamond_cost} \u{1F48E})
282
+ ${footer}`;
283
+ }
284
+ if (start.project_code) {
285
+ const proj = `\u{1F4C1} Projeto: ${start.project_code}${start.project_name ? ` \u2014 ${start.project_name}` : ""}`;
286
+ footer = footer ? `${proj}
287
+ ${footer}` : proj;
288
+ }
289
+ return footer;
290
+ }
291
+ function outputUrls(status) {
292
+ return (status.output?.urls ?? []).filter((u) => typeof u === "string" && u.length > 0);
293
+ }
294
+ function mimeFromUrl(url) {
295
+ const u = url.toLowerCase();
296
+ if (u.includes(".webp")) return "image/webp";
297
+ if (u.includes(".jpg") || u.includes(".jpeg")) return "image/jpeg";
298
+ return "image/png";
299
+ }
300
+ async function buildImageBlocks(urls, cfg) {
301
+ if (!cfg.inlineImages) return [];
302
+ const blocks = [];
303
+ for (const url of urls.slice(0, cfg.inlineImageMax)) {
304
+ try {
305
+ const res = await fetch(url);
306
+ if (!res.ok) continue;
307
+ const mimeType = res.headers.get("content-type") || mimeFromUrl(url);
308
+ if (!mimeType.startsWith("image/")) continue;
309
+ const buf = Buffer.from(await res.arrayBuffer());
310
+ if (buf.byteLength > cfg.inlineImageMaxBytes) continue;
311
+ blocks.push({ type: "image", data: buf.toString("base64"), mimeType });
312
+ } catch (e) {
313
+ logError("failed to inline image", url, e?.message ?? "unknown");
314
+ }
315
+ }
316
+ return blocks;
317
+ }
318
+
319
+ // src/schemas/enums.ts
320
+ var IMAGE_MODELS = [
321
+ "google/nano-banana-2",
322
+ "google/nano-banana-pro",
323
+ "openai/gpt-image-2"
324
+ ];
325
+ var VIDEO_MODELS = [
326
+ "bytedance/seedance-2.0",
327
+ "bytedance/seedance-2.0-fast",
328
+ "kwaivgi/kling-v2.5-turbo-pro",
329
+ "kwaivgi/kling-v2.6",
330
+ "kwaivgi/kling-v2.6-motion-control",
331
+ "kwaivgi/kling-v3-video",
332
+ "kwaivgi/kling-v3-omni-video"
333
+ ];
334
+ var MUSIC_MODELS = [
335
+ "suno/v5-5",
336
+ "suno/v4-5"
337
+ ];
338
+ var VOCAL_GENDERS = ["f", "m"];
339
+ var TTS_MODELS = [
340
+ "gemini-3.1-flash-tts-preview",
341
+ "gemini-2.5-pro-preview-tts"
342
+ ];
343
+ var VOICES = [
344
+ "Zephyr",
345
+ "Puck",
346
+ "Charon",
347
+ "Kore",
348
+ "Fenrir",
349
+ "Leda",
350
+ "Orus",
351
+ "Aoede",
352
+ "Callirrhoe",
353
+ "Autonoe",
354
+ "Enceladus",
355
+ "Iapetus",
356
+ "Umbriel",
357
+ "Algieba",
358
+ "Despina",
359
+ "Erinome",
360
+ "Algenib",
361
+ "Rasalgethi",
362
+ "Laomedeia",
363
+ "Achernar",
364
+ "Alnilam",
365
+ "Schedar",
366
+ "Gacrux",
367
+ "Pulcherrima",
368
+ "Achird",
369
+ "Zubenelgenubi",
370
+ "Vindemiatrix",
371
+ "Sadachbia",
372
+ "Sadaltager",
373
+ "Sulafat"
374
+ ];
375
+ var ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9"];
376
+ var IMAGE_RESOLUTIONS = ["1K", "2K", "4K"];
377
+ var VIDEO_RESOLUTIONS = ["480p", "720p", "1080p", "2K"];
378
+ var DEFAULTS = {
379
+ imageModel: "google/nano-banana-2",
380
+ imageAspect: "1:1",
381
+ imageResolution: "1K",
382
+ videoModel: "bytedance/seedance-2.0-fast",
383
+ videoAspect: "16:9",
384
+ videoResolution: "720p",
385
+ videoDuration: 5,
386
+ ttsModel: "gemini-3.1-flash-tts-preview",
387
+ voice: "Kore",
388
+ musicModel: "suno/v5-5"
389
+ };
390
+
391
+ // src/tools/generation.ts
392
+ var IMAGE_BUDGET_MS = 9e4;
393
+ var VIDEO_BUDGET_MS = 12e4;
394
+ function registerGenerationTools(server, client, cfg) {
395
+ server.registerTool(
396
+ "generate_image",
397
+ {
398
+ title: "Gerar imagem",
399
+ description: "Gera uma imagem a partir de um prompt usando o IA Lab da BePlus. R\xE1pido (~10-30s). Consome diamantes da conta vinculada. Apenas `prompt` \xE9 obrigat\xF3rio; passe `reference_images` (URLs p\xFAblicas) para image-to-image. Veja custos com estimate_cost / list_models." + TEAM_GATING_NOTE,
400
+ inputSchema: {
401
+ prompt: z.string().min(1).max(4e3).describe("Descri\xE7\xE3o da imagem desejada."),
402
+ model: z.enum(IMAGE_MODELS).default(DEFAULTS.imageModel).describe("nano-banana-2 = barato/r\xE1pido; nano-banana-pro = melhor qualidade; gpt-image-2 = OpenAI."),
403
+ aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Propor\xE7\xE3o (nano-banana). Padr\xE3o 1:1."),
404
+ resolution: z.enum(IMAGE_RESOLUTIONS).optional().describe("Resolu\xE7\xE3o (nano-banana): 1K/2K/4K. Padr\xE3o 1K."),
405
+ reference_images: z.array(z.string().url()).max(16).optional().describe("URLs p\xFAblicas de imagens de refer\xEAncia (image-to-image). At\xE9 14 (nano-banana) / 16 (gpt-image-2)."),
406
+ google_search: z.boolean().optional().describe("nano-banana-2: aterra a imagem em busca web do Google (fatos atuais). Padr\xE3o false."),
407
+ image_search: z.boolean().optional().describe("nano-banana-2: usa busca de imagens do Google como refer\xEAncia visual. Padr\xE3o false."),
408
+ size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional().describe("Tamanho (gpt-image-2)."),
409
+ quality: z.enum(["low", "medium", "high", "auto"]).optional().describe("Qualidade (gpt-image-2)."),
410
+ background: z.enum(["opaque", "transparent", "auto"]).optional().describe("Fundo (gpt-image-2). transparent s\xF3 com output_format png/webp. Padr\xE3o opaque."),
411
+ moderation: z.enum(["low", "auto"]).optional().describe("Modera\xE7\xE3o (gpt-image-2). Padr\xE3o low."),
412
+ output_format: z.enum(["png", "jpeg", "webp"]).optional().describe("Formato de sa\xEDda (gpt-image-2). Padr\xE3o png."),
413
+ output_compression: z.number().int().min(0).max(100).optional().describe("Compress\xE3o 0-100 (gpt-image-2, s\xF3 jpeg/webp). Padr\xE3o 90."),
414
+ seed: z.number().int().optional(),
415
+ project: z.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.')
416
+ }
417
+ },
418
+ async (args) => {
419
+ try {
420
+ const isGoogle = args.model.startsWith("google/");
421
+ const input = compact({
422
+ prompt: args.prompt,
423
+ aspect_ratio: args.aspect_ratio ?? (isGoogle ? DEFAULTS.imageAspect : void 0),
424
+ resolution: args.resolution ?? (isGoogle ? DEFAULTS.imageResolution : void 0),
425
+ reference_images: args.reference_images,
426
+ google_search: args.google_search,
427
+ image_search: args.image_search,
428
+ size: args.size,
429
+ quality: args.quality,
430
+ background: args.background,
431
+ moderation: args.moderation,
432
+ output_format: args.output_format,
433
+ output_compression: args.output_compression,
434
+ seed: args.seed
435
+ });
436
+ const start = await client.startGeneration(
437
+ compact({ model: args.model, input, project: args.project ?? cfg.activeProject ?? void 0 })
438
+ );
439
+ if (!start.success || !start.generation_id) {
440
+ if (isTeamGatingError(start.message)) return errorResult(await buildPickProjectMessage(client));
441
+ return errorResult(start.message || "Falha ao iniciar a gera\xE7\xE3o.");
442
+ }
443
+ const footer = costFooter(start, cfg);
444
+ const { settled, status } = await pollUntilDone(client, start.generation_id, IMAGE_BUDGET_MS);
445
+ if (!settled) {
446
+ return textResult(
447
+ `\u23F3 Imagem ainda em processamento (id: ${start.generation_id}, ${status.progress ?? 0}%). Use check_generation com esse id.
448
+ ${footer}`
449
+ );
450
+ }
451
+ if (status.status === "failed") {
452
+ return errorResult(`Gera\xE7\xE3o falhou: ${status.error_message || "erro desconhecido"}.
453
+ ${footer}`);
454
+ }
455
+ const urls = outputUrls(status);
456
+ const blocks = await buildImageBlocks(urls, cfg);
457
+ const list = urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
458
+ return {
459
+ content: [{ type: "text", text: `\u2705 Imagem gerada (${args.model}).
460
+ ${list}
461
+ ${footer}` }, ...blocks]
462
+ };
463
+ } catch (e) {
464
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "";
465
+ if (isTeamGatingError(msg)) return errorResult(await buildPickProjectMessage(client));
466
+ return errorResult(apiErrorToUserMessage(e));
467
+ }
468
+ }
469
+ );
470
+ server.registerTool(
471
+ "generate_video",
472
+ {
473
+ title: "Gerar v\xEDdeo",
474
+ description: "Gera um v\xEDdeo curto usando o IA Lab da BePlus. LENTO (geralmente 1-5+ min). Aguarda at\xE9 ~2 min; se n\xE3o terminar, retorna o generation_id e o status \u2014 ent\xE3o use check_generation. Consome diamantes (mais caro para dura\xE7\xF5es/resolu\xE7\xF5es maiores). Suporta image-to-video (`first_frame_image`), \xFAltimo frame (`last_frame_image`), refer\xEAncias de imagem/v\xEDdeo/\xE1udio, prompt negativo, \xE1udio gerado e mais \u2014 os campos aceitos variam por modelo (consulte list_models). `kling-v2.6-motion-control` usa `first_frame_image` + `motion_video`. Imagens com rosto de pessoa REAL s\xE3o bloqueadas pela modera\xE7\xE3o do provedor (use ilustra\xE7\xF5es/personagens)." + TEAM_GATING_NOTE,
475
+ inputSchema: {
476
+ prompt: z.string().min(1).max(4e3).describe("Descri\xE7\xE3o do v\xEDdeo desejado."),
477
+ model: z.enum(VIDEO_MODELS).default(DEFAULTS.videoModel).describe("seedance-2.0-fast = barato/r\xE1pido; seedance-2.0 = melhor; kling-v3-video = alternativa."),
478
+ aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Propor\xE7\xE3o. Padr\xE3o 16:9."),
479
+ resolution: z.enum(VIDEO_RESOLUTIONS).optional().describe("480p/720p/1080p/2K. Padr\xE3o 720p."),
480
+ duration: z.number().int().min(3).max(60).optional().describe(
481
+ "Dura\xE7\xE3o em segundos. Seedance 2.0/2.0-fast: 4\u201315 (granular). Kling: 5 ou 10. O backend valida por modelo \u2014 consulte list_models p/ os valores aceitos. Padr\xE3o 5."
482
+ ),
483
+ negative_prompt: z.string().max(2500).optional().describe("O que evitar no v\xEDdeo (Kling)."),
484
+ mode: z.enum(["standard", "pro"]).optional().describe("Qualidade de gera\xE7\xE3o (Kling): standard ou pro."),
485
+ generate_audio: z.boolean().optional().describe("Gerar trilha de \xE1udio junto com o v\xEDdeo (Seedance/Kling). Na web fica ligado por padr\xE3o."),
486
+ first_frame_image: z.string().url().optional().describe("URL da imagem inicial (image-to-video / primeiro frame)."),
487
+ last_frame_image: z.string().url().optional().describe("URL da imagem do \xFAltimo frame (Seedance/Kling)."),
488
+ reference_images: z.array(z.string().url()).max(9).optional().describe("URLs de imagens de refer\xEAncia (personagem/estilo) \u2014 modelos omni/Seedance. At\xE9 9."),
489
+ reference_videos: z.array(z.string().url()).max(4).optional().describe("URLs de v\xEDdeos de refer\xEAncia (Seedance reference_videos / Kling reference_video)."),
490
+ reference_audios: z.array(z.string().url()).max(4).optional().describe("URLs de \xE1udios de refer\xEAncia (Seedance)."),
491
+ motion_video: z.string().url().optional().describe("URL do v\xEDdeo de movimento/m\xE1scara din\xE2mica (kling-v2.6-motion-control)."),
492
+ character_orientation: z.string().optional().describe("Orienta\xE7\xE3o do personagem (alguns modelos Kling via Replicate)."),
493
+ keep_original_sound: z.boolean().optional().describe("Manter o som original do v\xEDdeo de refer\xEAncia (alguns modelos Kling)."),
494
+ reference_image: z.string().url().optional().describe("[Obsoleto \u2014 use first_frame_image] URL de imagem de refer\xEAncia (image-to-video)."),
495
+ seed: z.number().int().optional(),
496
+ project: z.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.')
497
+ }
498
+ },
499
+ async (args) => {
500
+ try {
501
+ const firstFrame = args.first_frame_image ?? args.reference_image;
502
+ const refVideos = args.reference_videos;
503
+ const input = compact({
504
+ prompt: args.prompt,
505
+ aspect_ratio: args.aspect_ratio ?? DEFAULTS.videoAspect,
506
+ resolution: args.resolution ?? DEFAULTS.videoResolution,
507
+ duration: args.duration ?? DEFAULTS.videoDuration,
508
+ negative_prompt: args.negative_prompt,
509
+ mode: args.mode,
510
+ generate_audio: args.generate_audio,
511
+ image: firstFrame,
512
+ end_image: args.last_frame_image,
513
+ reference_images: args.reference_images,
514
+ reference_videos: refVideos,
515
+ reference_video: refVideos && refVideos.length > 0 ? refVideos[0] : void 0,
516
+ reference_audios: args.reference_audios,
517
+ motion_video: args.motion_video,
518
+ character_orientation: args.character_orientation,
519
+ keep_original_sound: args.keep_original_sound,
520
+ seed: args.seed
521
+ });
522
+ const start = await client.startGeneration(
523
+ compact({ model: args.model, input, project: args.project ?? cfg.activeProject ?? void 0 })
524
+ );
525
+ if (!start.success || !start.generation_id) {
526
+ if (isTeamGatingError(start.message)) return errorResult(await buildPickProjectMessage(client));
527
+ return errorResult(start.message || "Falha ao iniciar a gera\xE7\xE3o.");
528
+ }
529
+ const footer = costFooter(start, cfg);
530
+ const { settled, status } = await pollUntilDone(client, start.generation_id, VIDEO_BUDGET_MS);
531
+ if (!settled) {
532
+ return textResult(
533
+ `\u23F3 V\xEDdeo ainda em processamento (id: ${start.generation_id}, ${status.progress ?? 0}%). V\xEDdeos costumam levar alguns minutos \u2014 use check_generation com esse id.
534
+ ${footer}`
535
+ );
536
+ }
537
+ if (status.status === "failed") {
538
+ return errorResult(`Gera\xE7\xE3o falhou: ${status.error_message || "erro desconhecido"}.
539
+ ${footer}`);
540
+ }
541
+ const urls = outputUrls(status);
542
+ const list = urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
543
+ return textResult(`\u2705 V\xEDdeo gerado (${args.model}).
544
+ ${list}
545
+ ${footer}`);
546
+ } catch (e) {
547
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "";
548
+ if (isTeamGatingError(msg)) return errorResult(await buildPickProjectMessage(client));
549
+ return errorResult(apiErrorToUserMessage(e));
550
+ }
551
+ }
552
+ );
553
+ server.registerTool(
554
+ "check_generation",
555
+ {
556
+ title: "Checar gera\xE7\xE3o",
557
+ description: "Consulta o status de uma gera\xE7\xE3o ass\xEDncrona (imagem/v\xEDdeo) pelo id. Use depois de generate_video ou de um generate_image que estourou o tempo. Retorna status, progresso e (quando pronto) as URLs.",
558
+ inputSchema: {
559
+ generation_id: z.string().min(1).describe("O id retornado por generate_image / generate_video.")
560
+ }
561
+ },
562
+ async ({ generation_id }) => {
563
+ try {
564
+ const status = await client.getStatus(generation_id);
565
+ if (status.status === "completed") {
566
+ const urls = outputUrls(status);
567
+ const blocks = await buildImageBlocks(urls, cfg);
568
+ const list = urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
569
+ return { content: [{ type: "text", text: `\u2705 Pronto.
570
+ ${list}` }, ...blocks] };
571
+ }
572
+ if (status.status === "failed") {
573
+ return errorResult(`Gera\xE7\xE3o falhou: ${status.error_message || "erro desconhecido"}.`);
574
+ }
575
+ return textResult(
576
+ `\u23F3 ${status.status} \u2014 ${status.progress ?? 0}%${status.stage ? ` (${status.stage})` : ""}. Tente novamente em alguns segundos.`
577
+ );
578
+ } catch (e) {
579
+ return errorResult(apiErrorToUserMessage(e));
580
+ }
581
+ }
582
+ );
583
+ server.registerTool(
584
+ "cancel_generation",
585
+ {
586
+ title: "Cancelar gera\xE7\xE3o",
587
+ description: "Cancela uma gera\xE7\xE3o em fila/processamento pelo id. Pode estornar diamantes.",
588
+ inputSchema: {
589
+ generation_id: z.string().min(1).describe("O id da gera\xE7\xE3o a cancelar.")
590
+ }
591
+ },
592
+ async ({ generation_id }) => {
593
+ try {
594
+ await client.cancel(generation_id);
595
+ return textResult(`\u2705 Gera\xE7\xE3o ${generation_id} cancelada (diamantes estornados quando aplic\xE1vel).`);
596
+ } catch (e) {
597
+ return errorResult(apiErrorToUserMessage(e));
598
+ }
599
+ }
600
+ );
601
+ const readMetadataSchema = {
602
+ url: z.string().url().optional().describe("URL p\xFAblica do arquivo gerado \u2014 imagem, v\xEDdeo, \xE1udio ou m\xFAsica (ex.: link do storage da BePlus)."),
603
+ image_base64: z.string().optional().describe("Conte\xFAdo do arquivo em base64 \u2014 use quando a m\xEDdia estiver na m\xE1quina do usu\xE1rio (leia o arquivo do disco e converta para base64). Alternativa \xE0 url.")
604
+ };
605
+ const readMetadataHandler = async ({ url, image_base64 }) => {
606
+ if (!url && !image_base64) return errorResult("Forne\xE7a url ou image_base64.");
607
+ try {
608
+ const r = await client.readProvenance(compact({ url, image_base64 }));
609
+ if (!r.found) {
610
+ return textResult(
611
+ "Nenhum dado de gera\xE7\xE3o encontrado nesse arquivo (sem metadata embutida ou formato n\xE3o suportado)."
612
+ );
613
+ }
614
+ const p = r.provenance;
615
+ const g = r.generation ?? void 0;
616
+ const model = g?.model ?? p?.model ?? "(modelo desconhecido)";
617
+ const prompt = g?.prompt ?? p?.prompt ?? "(sem prompt)";
618
+ const configs = g?.input_params ?? p?.configs ?? {};
619
+ const inputs = p?.input_image_urls ?? [];
620
+ const lines = [
621
+ `\u{1F9EC} Dados da gera\xE7\xE3o (${r.source === "db" ? "registro da conta" : "metadata do arquivo"}):`,
622
+ `\u2022 Modelo: ${model}`,
623
+ `\u2022 Prompt: ${prompt}`,
624
+ `\u2022 Configs: ${JSON.stringify(configs)}`
625
+ ];
626
+ if (inputs.length) lines.push(`\u2022 Inputs: ${inputs.join(", ")}`);
627
+ if (p?.generation_id) lines.push(`\u2022 generation_id: ${p.generation_id}`);
628
+ return textResult(lines.join("\n"));
629
+ } catch (e) {
630
+ return errorResult(apiErrorToUserMessage(e));
631
+ }
632
+ };
633
+ server.registerTool(
634
+ "read_media_metadata",
635
+ {
636
+ title: "Ler dados de gera\xE7\xE3o de uma m\xEDdia",
637
+ description: "L\xEA os dados de gera\xE7\xE3o embutidos em QUALQUER m\xEDdia gerada na plataforma BePlus \u2014 imagem, v\xEDdeo, \xE1udio ou m\xFAsica (prompt, modelo, configs, inputs). \xDAtil para recriar/ajustar uma gera\xE7\xE3o a partir do resultado. Passe o arquivo de UMA destas formas: (a) `url` p\xFAblica (ex.: link do storage da BePlus); ou (b) `image_base64` \u2014 se o arquivo estiver NA M\xC1QUINA DO USU\xC1RIO, leia-o do disco voc\xEA mesmo e envie o conte\xFAdo em base64 neste campo. O servidor NUNCA acessa o filesystem do usu\xE1rio; ler o arquivo local e convert\xEA-lo em base64 \xE9 responsabilidade do cliente (voc\xEA).",
638
+ inputSchema: readMetadataSchema
639
+ },
640
+ readMetadataHandler
641
+ );
642
+ server.registerTool(
643
+ "read_image_metadata",
644
+ {
645
+ title: "Ler dados de gera\xE7\xE3o de uma m\xEDdia (alias)",
646
+ description: "[Obsoleto \u2014 use read_media_metadata] L\xEA os dados de gera\xE7\xE3o embutidos em qualquer m\xEDdia gerada na BePlus (imagem, v\xEDdeo, \xE1udio ou m\xFAsica). Passe `url` p\xFAblica OU `image_base64` (se o arquivo estiver na m\xE1quina do usu\xE1rio, leia-o do disco e envie em base64).",
647
+ inputSchema: readMetadataSchema
648
+ },
649
+ readMetadataHandler
650
+ );
651
+ }
652
+
653
+ // src/tools/audio.ts
654
+ import { z as z2 } from "zod";
655
+ function registerAudioTools(server, client, cfg) {
656
+ server.registerTool(
657
+ "generate_audio",
658
+ {
659
+ title: "Gerar \xE1udio (TTS)",
660
+ description: 'Sintetiza fala a partir de texto (s\xEDncrono \u2014 retorna a URL na hora) usando o IA Lab da BePlus. ~30 vozes. Opcional `style_instruction` (notas de dire\xE7\xE3o, ex.: "fale animado e pausado") e di\xE1logo com 2 locutores via `multi_speaker`. Consome diamantes por segundo de \xE1udio.' + TEAM_GATING_NOTE,
661
+ inputSchema: {
662
+ text: z2.string().min(1).max(1e4).describe("Texto a ser falado."),
663
+ voice: z2.enum(VOICES).default(DEFAULTS.voice).describe("Voz (ex.: Kore, Puck, Charon)."),
664
+ model: z2.enum(TTS_MODELS).default(DEFAULTS.ttsModel).describe("Modelo TTS."),
665
+ style_instruction: z2.string().max(2e3).optional().describe("Instru\xE7\xE3o de estilo/tom aplicada \xE0 fala."),
666
+ multi_speaker: z2.object({
667
+ speaker1: z2.object({ name: z2.string().min(1).max(50), voice: z2.enum(VOICES) }),
668
+ speaker2: z2.object({ name: z2.string().min(1).max(50), voice: z2.enum(VOICES) })
669
+ }).optional().describe('Di\xE1logo de 2 locutores. Marque as falas no texto como "Nome: ...".'),
670
+ project: z2.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). Omita para usar o projeto ativo da conta.')
671
+ }
672
+ },
673
+ async (args) => {
674
+ try {
675
+ const body = compact({
676
+ text: args.text,
677
+ voice: args.voice,
678
+ model: args.model,
679
+ styleInstruction: args.style_instruction,
680
+ multiSpeaker: args.multi_speaker,
681
+ project: args.project ?? cfg.activeProject ?? void 0
682
+ });
683
+ const res = await client.tts(body);
684
+ const parts = [`Custo: ${res.diamond_cost} \u{1F48E}`];
685
+ if (res.diamonds_refunded) parts.push(`Estorno: ${res.diamonds_refunded} \u{1F48E}`);
686
+ const projLine = res.project_code ? `
687
+ \u{1F4C1} Projeto: ${res.project_code}${res.project_name ? ` \u2014 ${res.project_name}` : ""}` : "";
688
+ return textResult(
689
+ `\u2705 \xC1udio gerado (${res.model}, voz ${res.voice}, ${res.duration_sec.toFixed(1)}s).
690
+ ${res.url}
691
+ ${parts.join(" \xB7 ")}${projLine}`
692
+ );
693
+ } catch (e) {
694
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "";
695
+ if (isTeamGatingError(msg)) return errorResult(await buildPickProjectMessage(client));
696
+ return errorResult(apiErrorToUserMessage(e));
697
+ }
698
+ }
699
+ );
700
+ }
701
+
702
+ // src/tools/music.ts
703
+ import { z as z3 } from "zod";
704
+ var MUSIC_BUDGET_MS = 15e4;
705
+ function registerMusicTools(server, client, cfg) {
706
+ server.registerTool(
707
+ "generate_music",
708
+ {
709
+ title: "Gerar m\xFAsica",
710
+ description: 'Gera uma m\xFAsica completa (com vocais ou instrumental) usando o Suno no IA Lab da BePlus. ASS\xCDNCRONO e LENTO (geralmente 30s\u20132+ min). Aguarda at\xE9 ~2,5 min; se n\xE3o terminar, retorna o generation_id e o status \u2014 ent\xE3o use check_generation. Consome diamantes. DOIS modos: (a) DESCRI\xC7\xC3O (custom_mode=false, padr\xE3o) \u2192 passe `description` ("uma balada triste de piano"); (b) LETRA PR\xD3PRIA (custom_mode=true) \u2192 passe `lyrics` (a letra) e opcionalmente `title`. Use `tags` para g\xEAnero/estilo em ambos os modos (ex.: "sertanejo, animado, vocal feminino"). `vocal_gender` (f/m) s\xF3 \xE9 aceito pelo Suno v4.5 e \xE9 ignorado quando `make_instrumental=true`.' + TEAM_GATING_NOTE,
711
+ inputSchema: {
712
+ model: z3.enum(MUSIC_MODELS).default(DEFAULTS.musicModel).describe("suno/v5-5 = mais novo; suno/v4-5 = r\xE1pido/consistente (\xFAnico que aceita vocal_gender)."),
713
+ custom_mode: z3.boolean().optional().describe("false (padr\xE3o) = gera a partir de `description`; true = usa sua `lyrics` pr\xF3pria."),
714
+ description: z3.string().max(2500).optional().describe("Modo descri\xE7\xE3o (custom_mode=false): descreva a m\xFAsica desejada. Obrigat\xF3rio nesse modo."),
715
+ lyrics: z3.string().max(5e3).optional().describe("Modo letra (custom_mode=true): a letra completa da m\xFAsica. Obrigat\xF3rio nesse modo."),
716
+ title: z3.string().max(120).optional().describe("T\xEDtulo da m\xFAsica (modo letra)."),
717
+ tags: z3.string().max(1e3).optional().describe('G\xEAnero/estilo, separados por v\xEDrgula (ex.: "pop, energ\xE9tico, vocal masculino").'),
718
+ make_instrumental: z3.boolean().optional().describe("true = sem vocais (apenas instrumental). Padr\xE3o false."),
719
+ vocal_gender: z3.enum(VOCAL_GENDERS).optional().describe("G\xEAnero do vocal: f (feminino) ou m (masculino). S\xF3 Suno v4.5; ignorado se instrumental."),
720
+ negative_tags: z3.string().max(1e3).optional().describe("Estilos/elementos a evitar."),
721
+ style_weight: z3.number().min(0).max(1).optional().describe("Peso do estilo (0\u20131). Maior = segue mais as tags. Omita para o modelo decidir."),
722
+ weirdness_constraint: z3.number().min(0).max(1).optional().describe("Criatividade/imprevisibilidade (0\u20131). Omita para o modelo decidir."),
723
+ project: z3.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.')
724
+ }
725
+ },
726
+ async (args) => {
727
+ try {
728
+ const customMode = args.custom_mode ?? false;
729
+ if (customMode && !args.lyrics?.trim()) {
730
+ return errorResult("Modo letra (custom_mode=true) exige `lyrics`.");
731
+ }
732
+ if (!customMode && !args.description?.trim()) {
733
+ return errorResult("Modo descri\xE7\xE3o (custom_mode=false) exige `description`.");
734
+ }
735
+ const input = {
736
+ custom_mode: customMode,
737
+ make_instrumental: args.make_instrumental ?? false
738
+ };
739
+ if (args.vocal_gender) input.vocal_gender = args.vocal_gender;
740
+ if (args.negative_tags?.trim()) input.negative_tags = args.negative_tags.trim();
741
+ if (args.style_weight != null) input.style_weight = args.style_weight;
742
+ if (args.weirdness_constraint != null) input.weirdness_constraint = args.weirdness_constraint;
743
+ if (args.tags?.trim()) input.tags = args.tags.trim();
744
+ if (customMode) {
745
+ input.prompt = args.lyrics.trim();
746
+ if (args.title?.trim()) input.title = args.title.trim();
747
+ } else {
748
+ const desc = args.description.trim();
749
+ input.gpt_description_prompt = desc;
750
+ input.prompt = desc;
751
+ }
752
+ const start = await client.startGeneration(
753
+ compact({ model: args.model, input, project: args.project ?? cfg.activeProject ?? void 0 })
754
+ );
755
+ if (!start.success || !start.generation_id) {
756
+ if (isTeamGatingError(start.message)) return errorResult(await buildPickProjectMessage(client));
757
+ return errorResult(start.message || "Falha ao iniciar a gera\xE7\xE3o.");
758
+ }
759
+ const footer = costFooter(start, cfg);
760
+ const { settled, status } = await pollUntilDone(client, start.generation_id, MUSIC_BUDGET_MS);
761
+ if (!settled) {
762
+ return textResult(
763
+ `\u23F3 M\xFAsica ainda em produ\xE7\xE3o (id: ${start.generation_id}, ${status.progress ?? 0}%). Suno costuma levar alguns minutos \u2014 use check_generation com esse id.
764
+ ${footer}`
765
+ );
766
+ }
767
+ if (status.status === "failed") {
768
+ return errorResult(`Gera\xE7\xE3o falhou: ${status.error_message || "erro desconhecido"}.
769
+ ${footer}`);
770
+ }
771
+ const urls = outputUrls(status);
772
+ const list = urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
773
+ return textResult(`\u2705 M\xFAsica gerada (${args.model}).
774
+ ${list}
775
+ ${footer}`);
776
+ } catch (e) {
777
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "";
778
+ if (isTeamGatingError(msg)) return errorResult(await buildPickProjectMessage(client));
779
+ return errorResult(apiErrorToUserMessage(e));
780
+ }
781
+ }
782
+ );
783
+ }
784
+
785
+ // src/tools/account.ts
786
+ import { z as z4 } from "zod";
787
+ function registerAccountTools(server, client, _cfg) {
788
+ server.registerTool(
789
+ "whoami",
790
+ {
791
+ title: "Conta vinculada",
792
+ description: "Verifica o token (PAT) e mostra a conta BePlus vinculada (nome, email, saldo). Use para confirmar o v\xEDnculo.",
793
+ inputSchema: {}
794
+ },
795
+ async () => {
796
+ try {
797
+ const me = await client.me();
798
+ return textResult(
799
+ `\u{1F464} ${me.name ?? "(sem nome)"} <${me.email ?? "sem email"}>
800
+ Saldo: ${me.diamonds} \u{1F48E} \xB7 auth: ${me.authType}`
801
+ );
802
+ } catch (e) {
803
+ return errorResult(apiErrorToUserMessage(e));
804
+ }
805
+ }
806
+ );
807
+ server.registerTool(
808
+ "get_balance",
809
+ {
810
+ title: "Saldo de diamantes",
811
+ description: "Mostra o saldo atual de diamantes da conta vinculada.",
812
+ inputSchema: {}
813
+ },
814
+ async () => {
815
+ try {
816
+ const b = await client.balance();
817
+ return textResult(`Saldo: ${b.diamonds} \u{1F48E}`);
818
+ } catch (e) {
819
+ return errorResult(apiErrorToUserMessage(e));
820
+ }
821
+ }
822
+ );
823
+ server.registerTool(
824
+ "list_models",
825
+ {
826
+ title: "Listar modelos",
827
+ description: "Lista os modelos dispon\xEDveis com pre\xE7o-base e limites por usu\xE1rio. Use quando estiver em d\xFAvida sobre qual modelo/params usar ou quanto custa. \xC9 a fonte de verdade em runtime.",
828
+ inputSchema: {
829
+ kind: z4.enum(["image", "video", "audio", "all"]).default("all").optional().describe("Filtrar por tipo.")
830
+ }
831
+ },
832
+ async () => {
833
+ try {
834
+ const m = await client.models();
835
+ const lines = m.models.map((x) => {
836
+ const price = typeof x.pricing === "number" ? `$${x.pricing}` : JSON.stringify(x.pricing);
837
+ return `\u2022 ${x.model} \u2014 ${price} (base USD)`;
838
+ });
839
+ const analyzeLines = (m.analyze ?? []).map(
840
+ (a) => `\u2022 ${a.model} (${a.label}) \u2014 $${a.inputCostPerMillion}/$${a.outputCostPerMillion} por 1M tok in/out \xB7 aceita img=${a.accepts.images} vid=${a.accepts.videos} \xE1udio=${a.accepts.audios} pdf=${a.accepts.pdfs}`
841
+ );
842
+ const analyzeBlock = analyzeLines.length ? `
843
+
844
+ Modelos de an\xE1lise multimodal (tool analyze_media \u2014 prompt + m\xEDdia \u2192 texto):
845
+ ${analyzeLines.join("\n")}` : "";
846
+ const txt = `Modelos de gera\xE7\xE3o:
847
+ ${lines.join("\n") || "(nenhum)"}` + analyzeBlock + `
848
+
849
+ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xEDdeo=${m.limits.perUserMaxVideo}, \xE1udio=${m.limits.perUserMaxAudio}.
850
+ 1 \u{1F48E} \u2248 $${m.pricing.diamondValueUsd} (margem ${m.pricing.diamondMargin}x, m\xEDn ${m.pricing.minDiamonds} \u{1F48E}). Use estimate_cost para o custo exato em diamantes (gera\xE7\xE3o).`;
851
+ return textResult(txt);
852
+ } catch (e) {
853
+ return errorResult(apiErrorToUserMessage(e));
854
+ }
855
+ }
856
+ );
857
+ server.registerTool(
858
+ "estimate_cost",
859
+ {
860
+ title: "Estimar custo",
861
+ description: "Retorna o custo autoritativo em diamantes ANTES de gerar. Para \xE1udio (TTS) o custo \xE9 por segundo e s\xF3 \xE9 conhecido ap\xF3s a s\xEDntese.",
862
+ inputSchema: {
863
+ model: z4.string().min(1).describe("Id do modelo (ex.: google/nano-banana-pro, bytedance/seedance-2.0)."),
864
+ resolution: z4.string().optional(),
865
+ duration: z4.number().int().optional(),
866
+ mode: z4.string().optional(),
867
+ size: z4.string().optional(),
868
+ quality: z4.string().optional()
869
+ }
870
+ },
871
+ async (args) => {
872
+ try {
873
+ const c = await client.calculateCost(
874
+ compact({
875
+ model: args.model,
876
+ resolution: args.resolution,
877
+ duration: args.duration,
878
+ mode: args.mode,
879
+ size: args.size,
880
+ quality: args.quality
881
+ })
882
+ );
883
+ const note = c.fallback ? " [modelo desconhecido \u2014 custo m\xEDnimo]" : "";
884
+ return textResult(`Custo estimado: ${c.cost} \u{1F48E} (${c.model})${note}`);
885
+ } catch (e) {
886
+ return errorResult(apiErrorToUserMessage(e));
887
+ }
888
+ }
889
+ );
890
+ }
891
+
892
+ // src/tools/analyze.ts
893
+ import { z as z5 } from "zod";
894
+ function registerAnalyzeTools(server, client, _cfg) {
895
+ server.registerTool(
896
+ "analyze_media",
897
+ {
898
+ title: "Analisar m\xEDdia (Gemini)",
899
+ description: "Envia um prompt + m\xEDdia (imagem, v\xEDdeo, \xE1udio ou PDF) para o Gemini e devolve uma resposta em texto. Use para descrever/analisar/transcrever/resumir/extrair informa\xE7\xE3o de m\xEDdia, ou perguntar algo sobre ela. A m\xEDdia entra como `media_urls` (URLs p\xFAblicas \u2014 tudo que o Google aceita: imagens, v\xEDdeos, \xE1udios, PDFs). V\xEDdeo/\xE1udio grandes funcionam (v\xE3o pela Files API do Google). Consome diamantes (reembolsados se falhar). Veja custo/limites com list_models.",
900
+ inputSchema: {
901
+ prompt: z5.string().min(1).max(5e4).describe('O que voc\xEA quer do modelo sobre a m\xEDdia (ex.: "descreva este v\xEDdeo", "transcreva este \xE1udio").'),
902
+ media_urls: z5.array(z5.string().url()).max(50).optional().describe("URLs p\xFAblicas da m\xEDdia (imagem/v\xEDdeo/\xE1udio/PDF). Pode misturar tipos. Opcional (prompt s\xF3-texto tamb\xE9m funciona)."),
903
+ model: z5.enum(["gemini-flash", "gemini-pro"]).default("gemini-flash").optional().describe("gemini-flash = r\xE1pido/barato (Gemini 3.5 Flash); gemini-pro = mais capaz (Gemini 3.1 Pro).")
904
+ }
905
+ },
906
+ async (args) => {
907
+ try {
908
+ const res = await client.analyzeMedia(
909
+ compact({
910
+ prompt: args.prompt,
911
+ media_urls: args.media_urls,
912
+ model: args.model
913
+ })
914
+ );
915
+ const footer = `
916
+
917
+ \u2014 ${res.diamondsUsed} \u{1F48E} (saldo: ${res.newBalance}) \xB7 ${res.model} \xB7 ${res.inputTokens}\u2192${res.outputTokens} tok`;
918
+ return textResult(`${res.text}${footer}`);
919
+ } catch (e) {
920
+ return errorResult(apiErrorToUserMessage(e));
921
+ }
922
+ }
923
+ );
924
+ }
925
+
926
+ // src/tools/projects.ts
927
+ import { z as z6 } from "zod";
928
+ function projectLine(p, active) {
929
+ const marker = active ? "\u25CF " : "\u25CB ";
930
+ const counts = `${p.generation_count ?? 0} gera\xE7\xF5es`;
931
+ const owner = p.client_name ? ` \xB7 cliente: ${p.client_name}` : "";
932
+ return `${marker}${p.code} \u2014 ${p.name} (${counts}${owner})`;
933
+ }
934
+ function registerProjectTools(server, client, _cfg) {
935
+ server.registerTool(
936
+ "list_projects",
937
+ {
938
+ title: "Listar projetos",
939
+ description: "Lista os projetos do IA Lab em que a conta vinculada \xE9 membro, marcando qual est\xE1 ativo (\u25CF). Toda gera\xE7\xE3o feita sem `project` expl\xEDcito \xE9 vinculada ao projeto ativo. Use set_active_project para trocar.",
940
+ inputSchema: {}
941
+ },
942
+ async () => {
943
+ try {
944
+ const { projects } = await client.listProjects();
945
+ if (!projects.length) {
946
+ return textResult(
947
+ "Voc\xEA ainda n\xE3o \xE9 membro de nenhum projeto. Crie um com create_project, ou pe\xE7a a um admin para te adicionar."
948
+ );
949
+ }
950
+ const lines = projects.map((p) => projectLine(p, !!p.is_active_for_user));
951
+ const active = projects.find((p) => p.is_active_for_user);
952
+ const header = active ? `Projeto ativo: ${active.code} \u2014 ${active.name}` : "Nenhum projeto ativo (gera\xE7\xF5es ficam sem projeto).";
953
+ return textResult(`${header}
954
+
955
+ ${lines.join("\n")}`);
956
+ } catch (e) {
957
+ return errorResult(apiErrorToUserMessage(e));
958
+ }
959
+ }
960
+ );
961
+ server.registerTool(
962
+ "create_project",
963
+ {
964
+ title: "Criar projeto",
965
+ description: "Cria um projeto novo no IA Lab (workspace compartilhado), SEMPRE dentro de um cliente/marca, e por padr\xE3o j\xE1 o deixa ativo \u2014 gera\xE7\xF5es seguintes entram nele. S\xF3 membros da equipe podem criar. O campo `client` \xE9 obrigat\xF3rio: passe o code/uuid de um cliente existente (veja list_clients) OU o NOME de um cliente novo (ele \xE9 criado automaticamente). Projetos s\xE3o globais: todos da equipe usam.",
966
+ inputSchema: {
967
+ name: z6.string().min(1).max(256).describe('Nome do projeto (ex.: "Ladeira Abaixo").'),
968
+ client: z6.string().min(1).max(256).describe('Cliente/marca dono: code/uuid existente (ex.: "RDBL") OU nome novo (ex.: "Red Bull"). Obrigat\xF3rio.'),
969
+ description: z6.string().max(4e3).optional().describe("Descri\xE7\xE3o do projeto (opcional)."),
970
+ color: z6.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Cor em hex #RRGGBB para o badge (opcional)."),
971
+ set_active: z6.boolean().optional().describe("Deixar o novo projeto como ativo (padr\xE3o true).")
972
+ }
973
+ },
974
+ async (args) => {
975
+ try {
976
+ const { project, is_active } = await client.createProject({
977
+ name: args.name,
978
+ client: args.client,
979
+ description: args.description,
980
+ color: args.color,
981
+ set_active: args.set_active
982
+ });
983
+ const activeNote = is_active ? " (definido como ativo)" : "";
984
+ return textResult(`\u2705 Projeto criado: ${project.code} \u2014 ${project.name}${activeNote}.`);
985
+ } catch (e) {
986
+ return errorResult(apiErrorToUserMessage(e));
987
+ }
988
+ }
989
+ );
990
+ server.registerTool(
991
+ "set_active_project",
992
+ {
993
+ title: "Definir projeto ativo",
994
+ description: 'Define o projeto ativo da conta (por uuid OU code curto, ex.: "VRAO-26"). A partir da\xED, gera\xE7\xF5es sem `project` expl\xEDcito s\xE3o vinculadas a ele \u2014 espelha o seletor de projeto da web. Passe project=null para limpar.',
995
+ inputSchema: {
996
+ project: z6.string().max(64).nullable().describe("uuid OU code curto do projeto. null para nenhum projeto.")
997
+ }
998
+ },
999
+ async ({ project }) => {
1000
+ try {
1001
+ const { project: active } = await client.setActiveProject(project ?? null);
1002
+ if (!active) return textResult("\u2705 Projeto ativo removido \u2014 novas gera\xE7\xF5es ficar\xE3o sem projeto.");
1003
+ return textResult(`\u2705 Projeto ativo: ${active.code} \u2014 ${active.name}.`);
1004
+ } catch (e) {
1005
+ return errorResult(apiErrorToUserMessage(e));
1006
+ }
1007
+ }
1008
+ );
1009
+ }
1010
+
1011
+ // src/tools/clients.ts
1012
+ import { z as z7 } from "zod";
1013
+ function clientLine(c, active) {
1014
+ const marker = active ? "\u25CF " : "\u25CB ";
1015
+ const counts = `${c.project_count ?? 0} projetos`;
1016
+ return `${marker}${c.code} \u2014 ${c.name} (${counts})`;
1017
+ }
1018
+ function registerClientTools(server, client, _cfg) {
1019
+ server.registerTool(
1020
+ "list_clients",
1021
+ {
1022
+ title: "Listar clientes",
1023
+ description: "Lista os clientes/marcas do IA Lab (globais para a equipe), marcando qual est\xE1 ativo (\u25CF). Cliente \xE9 o container ACIMA do projeto (Cliente \u2192 Projeto \u2192 gera\xE7\xE3o). Use antes de criar/escolher um projeto: primeiro o cliente, depois o projeto dele.",
1024
+ inputSchema: {}
1025
+ },
1026
+ async () => {
1027
+ try {
1028
+ const { clients } = await client.listClients();
1029
+ if (!clients.length) {
1030
+ return textResult(
1031
+ 'Nenhum cliente ainda. Crie um com create_client (ex.: "Red Bull"), ou crie um projeto passando o nome do cliente em create_project \u2014 ele \xE9 criado junto.'
1032
+ );
1033
+ }
1034
+ const lines = clients.map((c) => clientLine(c, !!c.is_active_for_user));
1035
+ const active = clients.find((c) => c.is_active_for_user);
1036
+ const header = active ? `Cliente ativo: ${active.code} \u2014 ${active.name}` : "Nenhum cliente ativo.";
1037
+ return textResult(`${header}
1038
+
1039
+ ${lines.join("\n")}`);
1040
+ } catch (e) {
1041
+ return errorResult(apiErrorToUserMessage(e));
1042
+ }
1043
+ }
1044
+ );
1045
+ server.registerTool(
1046
+ "create_client",
1047
+ {
1048
+ title: "Criar cliente",
1049
+ description: 'Cria um cliente/marca novo (ex.: "Red Bull"). S\xF3 membros da equipe. Clientes s\xE3o globais: qualquer colega passa a v\xEA-lo e us\xE1-lo. Depois crie projetos dentro dele com create_project.',
1050
+ inputSchema: {
1051
+ name: z7.string().min(1).max(256).describe('Nome do cliente/marca (ex.: "Red Bull").')
1052
+ }
1053
+ },
1054
+ async (args) => {
1055
+ try {
1056
+ const { client: created } = await client.createClient({ name: args.name });
1057
+ return textResult(`\u2705 Cliente criado: ${created.code} \u2014 ${created.name}.`);
1058
+ } catch (e) {
1059
+ return errorResult(apiErrorToUserMessage(e));
1060
+ }
1061
+ }
1062
+ );
1063
+ server.registerTool(
1064
+ "set_active_client",
1065
+ {
1066
+ title: "Definir cliente ativo",
1067
+ description: 'Define o cliente ativo da conta (por uuid OU code curto, ex.: "RDBL"). Trocar de cliente limpa o projeto ativo se ele pertencia a outro cliente. Passe client=null para limpar.',
1068
+ inputSchema: {
1069
+ client: z7.string().max(64).nullable().describe("uuid OU code curto do cliente. null para nenhum.")
1070
+ }
1071
+ },
1072
+ async ({ client: ref }) => {
1073
+ try {
1074
+ const { client: active } = await client.setActiveClient(ref ?? null);
1075
+ if (!active) return textResult("\u2705 Cliente ativo removido.");
1076
+ return textResult(`\u2705 Cliente ativo: ${active.code} \u2014 ${active.name}.`);
1077
+ } catch (e) {
1078
+ return errorResult(apiErrorToUserMessage(e));
1079
+ }
1080
+ }
1081
+ );
1082
+ }
1083
+
1084
+ // src/tools/calls.ts
1085
+ import { z as z8 } from "zod";
1086
+ var VIS_LABEL = {
1087
+ team: "equipe",
1088
+ private: "privada",
1089
+ workspace: "projeto",
1090
+ link: "link p\xFAblico"
1091
+ };
1092
+ function fmtDuration(sec) {
1093
+ if (!sec || sec <= 0) return "\u2014";
1094
+ const m = Math.floor(sec / 60);
1095
+ const s = Math.floor(sec % 60);
1096
+ return `${m}:${String(s).padStart(2, "0")}`;
1097
+ }
1098
+ function fmtDate(iso) {
1099
+ return iso ? iso.slice(0, 10) : "";
1100
+ }
1101
+ function callLine(c) {
1102
+ const proj = c.project_name ? ` \xB7 ${c.client_name ? c.client_name + " / " : ""}${c.project_name}` : "";
1103
+ const who = c.speakers.length ? ` \xB7 ${c.speakers.join(", ")}` : c.participant_count ? ` \xB7 ${c.participant_count} falantes` : "";
1104
+ const vis = VIS_LABEL[c.visibility] ?? c.visibility;
1105
+ const flag = c.status !== "ready" ? ` [${c.status}]` : "";
1106
+ return `\u2022 ${c.title ?? "(sem t\xEDtulo)"}${flag}
1107
+ ${fmtDate(c.date)} \xB7 ${fmtDuration(c.duration_sec)} \xB7 ${vis}${proj}${who}
1108
+ id: ${c.id}`;
1109
+ }
1110
+ function renderCall(c) {
1111
+ const lines = [`# ${c.title ?? "(sem t\xEDtulo)"}`];
1112
+ if (c.subtitle) lines.push(c.subtitle);
1113
+ const meta = [];
1114
+ if (c.date) meta.push(fmtDate(c.date));
1115
+ meta.push(fmtDuration(c.duration_sec));
1116
+ meta.push(VIS_LABEL[c.visibility] ?? c.visibility);
1117
+ if (c.project_name) meta.push(`${c.client_name ? c.client_name + " / " : ""}${c.project_name}`);
1118
+ if (c.status !== "ready") meta.push(`status: ${c.status}`);
1119
+ lines.push(meta.join(" \xB7 "));
1120
+ if (c.participants.length) lines.push(`Falantes: ${c.participants.join(", ")}`);
1121
+ if (c.recording_url) lines.push(`Grava\xE7\xE3o: ${c.recording_url}`);
1122
+ else if (c.audio_url) lines.push(`\xC1udio: ${c.audio_url}`);
1123
+ if (c.summary) lines.push(`
1124
+ ## Resumo
1125
+ ${c.summary}`);
1126
+ if (c.transcript.length) {
1127
+ lines.push("\n## Transcri\xE7\xE3o");
1128
+ let lastSpeaker = "";
1129
+ for (const t of c.transcript) {
1130
+ if (t.speaker !== lastSpeaker) {
1131
+ lines.push(`
1132
+ ${t.speaker}:`);
1133
+ lastSpeaker = t.speaker;
1134
+ }
1135
+ lines.push(t.text);
1136
+ }
1137
+ } else {
1138
+ lines.push("\n(transcri\xE7\xE3o ainda n\xE3o dispon\xEDvel)");
1139
+ }
1140
+ return lines.join("\n");
1141
+ }
1142
+ function registerCallTools(server, client, _cfg) {
1143
+ server.registerTool(
1144
+ "search_calls",
1145
+ {
1146
+ title: "Buscar calls",
1147
+ description: "Busca nas calls (reuni\xF5es gravadas e transcritas) vis\xEDveis pra conta \u2014 suas, da equipe e compartilhadas. Todos os filtros s\xE3o opcionais e combin\xE1veis: `query` (texto no t\xEDtulo/projeto/cliente/falante), `project` (code/uuid/nome), `client` (code/nome), `visibility` (team|private|workspace|link), `speaker` (nome do falante). Retorna uma lista resumida \u2014 use get_call com o id pra ler a transcri\xE7\xE3o completa.",
1148
+ inputSchema: {
1149
+ query: z8.string().max(200).optional().describe("Texto livre \u2014 casa t\xEDtulo, projeto, cliente ou falante."),
1150
+ project: z8.string().max(64).optional().describe('Projeto: code (ex.: "CONTEUDO-INS"), uuid ou parte do nome.'),
1151
+ client: z8.string().max(128).optional().describe("Cliente: code ou parte do nome."),
1152
+ visibility: z8.enum(["team", "private", "workspace", "link"]).optional().describe("Privacidade: team (equipe), private (privada), workspace (projeto) ou link."),
1153
+ speaker: z8.string().max(128).optional().describe("Nome do falante."),
1154
+ limit: z8.number().int().min(1).max(50).optional().describe("M\xE1ximo de resultados (padr\xE3o 20).")
1155
+ }
1156
+ },
1157
+ async (args) => {
1158
+ try {
1159
+ const { calls, total } = await client.searchCalls(args);
1160
+ if (!calls.length) return textResult("Nenhuma call encontrada com esses filtros.");
1161
+ const header = total > calls.length ? `${calls.length} de ${total} calls:` : `${calls.length} call(s):`;
1162
+ return textResult(`${header}
1163
+
1164
+ ${calls.map(callLine).join("\n\n")}`);
1165
+ } catch (e) {
1166
+ return errorResult(apiErrorToUserMessage(e));
1167
+ }
1168
+ }
1169
+ );
1170
+ server.registerTool(
1171
+ "get_call",
1172
+ {
1173
+ title: "Ler call",
1174
+ description: "Retorna o conte\xFAdo completo de uma call pelo id (use search_calls pra achar): t\xEDtulo, resumo, falantes, projeto/cliente, links da grava\xE7\xE3o e a TRANSCRI\xC7\xC3O inteira por falante. Respeita a visibilidade \u2014 s\xF3 retorna o que a conta pode ver.",
1175
+ inputSchema: {
1176
+ id: z8.string().min(1).max(64).describe("id (uuid) da call, obtido via search_calls.")
1177
+ }
1178
+ },
1179
+ async ({ id }) => {
1180
+ try {
1181
+ const { call } = await client.getCall(id);
1182
+ return textResult(renderCall(call));
1183
+ } catch (e) {
1184
+ return errorResult(apiErrorToUserMessage(e));
1185
+ }
1186
+ }
1187
+ );
1188
+ }
1189
+
1190
+ // src/server.ts
1191
+ function buildServer(cfg) {
1192
+ const server = new McpServer({ name: "beplus", version: "0.8.0" });
1193
+ const client = new BeplusClient(cfg);
1194
+ registerGenerationTools(server, client, cfg);
1195
+ registerAudioTools(server, client, cfg);
1196
+ registerMusicTools(server, client, cfg);
1197
+ registerAccountTools(server, client, cfg);
1198
+ registerAnalyzeTools(server, client, cfg);
1199
+ registerProjectTools(server, client, cfg);
1200
+ registerClientTools(server, client, cfg);
1201
+ registerCallTools(server, client, cfg);
1202
+ return { server, client };
1203
+ }
1204
+
1205
+ // src/index.ts
1206
+ async function main() {
1207
+ let cfg;
1208
+ try {
1209
+ cfg = loadConfig();
1210
+ } catch (e) {
1211
+ logError(e.message);
1212
+ process.exit(1);
1213
+ }
1214
+ const { server, client } = buildServer(cfg);
1215
+ if (cfg.verifyOnStart) {
1216
+ try {
1217
+ const me = await client.me();
1218
+ logInfo(`linked as ${me.name ?? me.email ?? me.userId} (${me.diamonds} \u{1F48E})`);
1219
+ } catch (e) {
1220
+ logError("startup verify failed:", e?.message ?? "unknown");
1221
+ }
1222
+ }
1223
+ const transport = new StdioServerTransport();
1224
+ await server.connect(transport);
1225
+ logInfo(`ready \u2014 api=${cfg.baseUrl}`);
1226
+ }
1227
+ process.on("uncaughtException", (e) => {
1228
+ logError("uncaughtException", e?.message ?? String(e));
1229
+ process.exit(1);
1230
+ });
1231
+ process.on("unhandledRejection", (e) => {
1232
+ logError("unhandledRejection", String(e));
1233
+ process.exit(1);
1234
+ });
1235
+ main().catch((e) => {
1236
+ logError("fatal", e?.message ?? String(e));
1237
+ process.exit(1);
1238
+ });