beplus-mcp 0.8.0 → 0.9.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 +7 -0
  2. package/dist/index.js +134 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -89,6 +89,13 @@ Reinicie o cliente. Rode a tool **`whoami`** para confirmar o vínculo.
89
89
  | `estimate_cost` | Custo em diamantes antes de gerar. |
90
90
  | `read_image_metadata` / `analyze_media` | Lê a proveniência embutida numa imagem / analisa mídia (imagem/vídeo/áudio/PDF) com prompt. |
91
91
 
92
+ > **Referências de arquivo local (desde 0.8.1):** os campos de mídia — `reference_images`,
93
+ > `first_frame_image`, `last_frame_image`, `reference_videos`, `reference_audios`, `motion_video`
94
+ > (geração) e `media_urls` (`analyze_media`) — aceitam **URL pública OU caminho de arquivo local**
95
+ > (`/caminho/foto.png`, `~/Desktop/ref.jpg`, `file://…`). Caminhos locais sobem automaticamente
96
+ > pro R2 da BePlus e viram URL pública antes de chegar ao provider — não precisa mais hospedar a
97
+ > imagem você mesmo.
98
+
92
99
  ### Calls (reuniões gravadas e transcritas)
93
100
  | Tool | O que faz |
94
101
  |------|-----------|
package/dist/index.js CHANGED
@@ -245,6 +245,110 @@ async function pollUntilDone(client, generationId, budgetMs, jitterSeed = 0) {
245
245
  return { settled: last.status === "completed" || last.status === "failed", status: last };
246
246
  }
247
247
 
248
+ // src/media.ts
249
+ import { readFile } from "fs/promises";
250
+ import { basename, extname, isAbsolute, resolve } from "path";
251
+ import { homedir } from "os";
252
+ import { fileURLToPath } from "url";
253
+ var MIME_BY_EXT = {
254
+ ".png": "image/png",
255
+ ".jpg": "image/jpeg",
256
+ ".jpeg": "image/jpeg",
257
+ ".webp": "image/webp",
258
+ ".gif": "image/gif",
259
+ ".bmp": "image/bmp",
260
+ ".svg": "image/svg+xml",
261
+ ".mp4": "video/mp4",
262
+ ".mov": "video/quicktime",
263
+ ".webm": "video/webm",
264
+ ".m4v": "video/x-m4v",
265
+ ".mp3": "audio/mpeg",
266
+ ".wav": "audio/wav",
267
+ ".m4a": "audio/mp4",
268
+ ".aac": "audio/aac",
269
+ ".ogg": "audio/ogg",
270
+ ".flac": "audio/flac",
271
+ ".pdf": "application/pdf"
272
+ };
273
+ var MAX_UPLOAD_BYTES = 300 * 1024 * 1024;
274
+ var UPLOAD_TIMEOUT_MS = 12e4;
275
+ function isRemoteRef(ref) {
276
+ return /^https?:\/\//i.test(ref);
277
+ }
278
+ function toLocalPath(ref) {
279
+ if (ref.startsWith("file://")) return fileURLToPath(ref);
280
+ if (ref === "~") return homedir();
281
+ if (ref.startsWith("~/")) return resolve(homedir(), ref.slice(2));
282
+ return isAbsolute(ref) ? ref : resolve(process.cwd(), ref);
283
+ }
284
+ function mimeForPath(path) {
285
+ return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
286
+ }
287
+ async function uploadFile(cfg, ref) {
288
+ const abs = toLocalPath(ref);
289
+ let buf;
290
+ try {
291
+ buf = await readFile(abs);
292
+ } catch {
293
+ throw new ApiError(
294
+ 0,
295
+ `Arquivo local n\xE3o encontrado: "${ref}". Passe um caminho v\xE1lido ou uma URL p\xFAblica (http/https).`
296
+ );
297
+ }
298
+ const name = basename(abs);
299
+ if (buf.byteLength > MAX_UPLOAD_BYTES) {
300
+ throw new ApiError(
301
+ 413,
302
+ `"${name}" tem ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 acima do limite de 300MB para upload.`
303
+ );
304
+ }
305
+ const form = new FormData();
306
+ form.append("file", new Blob([buf], { type: mimeForPath(abs) }), name);
307
+ const controller = new AbortController();
308
+ const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
309
+ let res;
310
+ try {
311
+ res = await fetch(`${cfg.baseUrl}/api/v1/files/upload`, {
312
+ method: "POST",
313
+ headers: { Authorization: `Bearer ${cfg.token}` },
314
+ body: form,
315
+ signal: controller.signal
316
+ });
317
+ } catch (e) {
318
+ if (e?.name === "AbortError") {
319
+ throw new ApiError(408, `Tempo esgotado ao subir "${name}" para o R2.`);
320
+ }
321
+ throw new ApiError(0, `Falha de rede ao subir "${name}" para o R2. Confira BEPLUS_API_URL.`);
322
+ } finally {
323
+ clearTimeout(timer);
324
+ }
325
+ const text = await res.text();
326
+ let json;
327
+ if (text) {
328
+ try {
329
+ json = JSON.parse(text);
330
+ } catch {
331
+ }
332
+ }
333
+ if (!res.ok) {
334
+ const message = json?.message || res.statusText || `HTTP ${res.status}`;
335
+ throw new ApiError(res.status, `Upload de "${name}" falhou: ${message}`, json);
336
+ }
337
+ const fileUrl = json?.result?.file_url ?? json?.file_url;
338
+ if (!fileUrl) {
339
+ throw new ApiError(0, `O upload de "${name}" n\xE3o retornou uma URL p\xFAblica.`);
340
+ }
341
+ return fileUrl;
342
+ }
343
+ async function resolveRef(cfg, ref) {
344
+ if (!ref) return ref;
345
+ return isRemoteRef(ref) ? ref : uploadFile(cfg, ref);
346
+ }
347
+ async function resolveRefs(cfg, refs) {
348
+ if (!refs || refs.length === 0) return refs;
349
+ return Promise.all(refs.map((r) => isRemoteRef(r) ? Promise.resolve(r) : uploadFile(cfg, r)));
350
+ }
351
+
248
352
  // src/log.ts
249
353
  function fmt(a) {
250
354
  return typeof a === "string" ? a : JSON.stringify(a);
@@ -325,6 +429,7 @@ var IMAGE_MODELS = [
325
429
  var VIDEO_MODELS = [
326
430
  "bytedance/seedance-2.0",
327
431
  "bytedance/seedance-2.0-fast",
432
+ "bytedance/seedance-2.0-mini",
328
433
  "kwaivgi/kling-v2.5-turbo-pro",
329
434
  "kwaivgi/kling-v2.6",
330
435
  "kwaivgi/kling-v2.6-motion-control",
@@ -396,13 +501,15 @@ function registerGenerationTools(server, client, cfg) {
396
501
  "generate_image",
397
502
  {
398
503
  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,
504
+ 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 OU caminhos de arquivo local \u2014 o MCP sobe pro R2 automaticamente) para image-to-image. Veja custos com estimate_cost / list_models." + TEAM_GATING_NOTE,
400
505
  inputSchema: {
401
506
  prompt: z.string().min(1).max(4e3).describe("Descri\xE7\xE3o da imagem desejada."),
402
507
  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
508
  aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Propor\xE7\xE3o (nano-banana). Padr\xE3o 1:1."),
404
509
  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)."),
510
+ reference_images: z.array(z.string().min(1)).max(16).optional().describe(
511
+ "Imagens de refer\xEAncia (image-to-image): URL p\xFAblica OU caminho de arquivo local (o MCP faz upload pro R2 automaticamente). At\xE9 14 (nano-banana) / 16 (gpt-image-2)."
512
+ ),
406
513
  google_search: z.boolean().optional().describe("nano-banana-2: aterra a imagem em busca web do Google (fatos atuais). Padr\xE3o false."),
407
514
  image_search: z.boolean().optional().describe("nano-banana-2: usa busca de imagens do Google como refer\xEAncia visual. Padr\xE3o false."),
408
515
  size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional().describe("Tamanho (gpt-image-2)."),
@@ -418,11 +525,12 @@ function registerGenerationTools(server, client, cfg) {
418
525
  async (args) => {
419
526
  try {
420
527
  const isGoogle = args.model.startsWith("google/");
528
+ const reference_images = await resolveRefs(cfg, args.reference_images);
421
529
  const input = compact({
422
530
  prompt: args.prompt,
423
531
  aspect_ratio: args.aspect_ratio ?? (isGoogle ? DEFAULTS.imageAspect : void 0),
424
532
  resolution: args.resolution ?? (isGoogle ? DEFAULTS.imageResolution : void 0),
425
- reference_images: args.reference_images,
533
+ reference_images,
426
534
  google_search: args.google_search,
427
535
  image_search: args.image_search,
428
536
  size: args.size,
@@ -483,23 +591,27 @@ ${footer}` }, ...blocks]
483
591
  negative_prompt: z.string().max(2500).optional().describe("O que evitar no v\xEDdeo (Kling)."),
484
592
  mode: z.enum(["standard", "pro"]).optional().describe("Qualidade de gera\xE7\xE3o (Kling): standard ou pro."),
485
593
  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)."),
594
+ first_frame_image: z.string().min(1).optional().describe("Imagem inicial (image-to-video / primeiro frame): URL p\xFAblica OU caminho local (sobe pro R2)."),
595
+ last_frame_image: z.string().min(1).optional().describe("Imagem do \xFAltimo frame (Seedance/Kling): URL p\xFAblica OU caminho local (sobe pro R2)."),
596
+ reference_images: z.array(z.string().min(1)).max(9).optional().describe("Imagens de refer\xEAncia (personagem/estilo) \u2014 omni/Seedance. URL p\xFAblica OU caminho local. At\xE9 9."),
597
+ reference_videos: z.array(z.string().min(1)).max(4).optional().describe("V\xEDdeos de refer\xEAncia (Seedance reference_videos / Kling reference_video): URL OU caminho local."),
598
+ reference_audios: z.array(z.string().min(1)).max(4).optional().describe("\xC1udios de refer\xEAncia (Seedance): URL p\xFAblica OU caminho local (sobe pro R2)."),
599
+ motion_video: z.string().min(1).optional().describe("V\xEDdeo de movimento/m\xE1scara din\xE2mica (kling-v2.6-motion-control): URL OU caminho local."),
492
600
  character_orientation: z.string().optional().describe("Orienta\xE7\xE3o do personagem (alguns modelos Kling via Replicate)."),
493
601
  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)."),
602
+ reference_image: z.string().min(1).optional().describe("[Obsoleto \u2014 use first_frame_image] Imagem de refer\xEAncia: URL p\xFAblica OU caminho local."),
495
603
  seed: z.number().int().optional(),
496
604
  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
605
  }
498
606
  },
499
607
  async (args) => {
500
608
  try {
501
- const firstFrame = args.first_frame_image ?? args.reference_image;
502
- const refVideos = args.reference_videos;
609
+ const firstFrame = await resolveRef(cfg, args.first_frame_image ?? args.reference_image);
610
+ const endImage = await resolveRef(cfg, args.last_frame_image);
611
+ const referenceImages = await resolveRefs(cfg, args.reference_images);
612
+ const referenceAudios = await resolveRefs(cfg, args.reference_audios);
613
+ const motionVideo = await resolveRef(cfg, args.motion_video);
614
+ const refVideos = await resolveRefs(cfg, args.reference_videos);
503
615
  const input = compact({
504
616
  prompt: args.prompt,
505
617
  aspect_ratio: args.aspect_ratio ?? DEFAULTS.videoAspect,
@@ -509,12 +621,12 @@ ${footer}` }, ...blocks]
509
621
  mode: args.mode,
510
622
  generate_audio: args.generate_audio,
511
623
  image: firstFrame,
512
- end_image: args.last_frame_image,
513
- reference_images: args.reference_images,
624
+ end_image: endImage,
625
+ reference_images: referenceImages,
514
626
  reference_videos: refVideos,
515
627
  reference_video: refVideos && refVideos.length > 0 ? refVideos[0] : void 0,
516
- reference_audios: args.reference_audios,
517
- motion_video: args.motion_video,
628
+ reference_audios: referenceAudios,
629
+ motion_video: motionVideo,
518
630
  character_orientation: args.character_orientation,
519
631
  keep_original_sound: args.keep_original_sound,
520
632
  seed: args.seed
@@ -891,7 +1003,7 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
891
1003
 
892
1004
  // src/tools/analyze.ts
893
1005
  import { z as z5 } from "zod";
894
- function registerAnalyzeTools(server, client, _cfg) {
1006
+ function registerAnalyzeTools(server, client, cfg) {
895
1007
  server.registerTool(
896
1008
  "analyze_media",
897
1009
  {
@@ -899,16 +1011,19 @@ function registerAnalyzeTools(server, client, _cfg) {
899
1011
  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
1012
  inputSchema: {
901
1013
  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)."),
1014
+ media_urls: z5.array(z5.string().min(1)).max(50).optional().describe(
1015
+ "M\xEDdia (imagem/v\xEDdeo/\xE1udio/PDF): URLs p\xFAblicas OU caminhos de arquivo local (o MCP sobe pro R2 automaticamente). Pode misturar tipos. Opcional (prompt s\xF3-texto tamb\xE9m funciona)."
1016
+ ),
903
1017
  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
1018
  }
905
1019
  },
906
1020
  async (args) => {
907
1021
  try {
1022
+ const media_urls = await resolveRefs(cfg, args.media_urls);
908
1023
  const res = await client.analyzeMedia(
909
1024
  compact({
910
1025
  prompt: args.prompt,
911
- media_urls: args.media_urls,
1026
+ media_urls,
912
1027
  model: args.model
913
1028
  })
914
1029
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beplus-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Conector MCP da equipe BePlus — gere imagens/vídeos/áudio e consulte calls, projetos e clientes pela sua conta BePlus (diamantes, limites e histórico aplicados pela plataforma).",
5
5
  "type": "module",
6
6
  "bin": {